Compare commits

..

7 Commits

Author SHA1 Message Date
jonlwheat2-gif
c107303286 Merge remote-tracking branch 'origin/release/v3.8.50' into pr-10610-local
# Conflicts:
#	scripts/build/colocate-standalone.mjs
2026-08-18 16:24:30 -03:00
backryun
df90591415 feat(providers): refresh curated model catalogs and retire Imagen 4 (#10537)
* feat(providers): refresh Gemini Flash catalogs and pricing

* fix(providers): refresh gemini-web Flash catalog

* chore(providers): eliminate Gemini 3.5/3.6 Flash models

* feat(providers): refresh Perplexity Web model mappings

* feat(providers): refresh PromptQL and Notion catalogs

* feat(providers): refresh KIE TinyCMS and Conol catalogs

* feat(providers): refresh OpenCode Zen catalog

* chore(providers): finish Gemini Flash cleanup

* chore(providers): retire Google Imagen 4
2026-08-18 12:27:46 -03:00
adevwithpurpose
08860f5cae fix(docs): remove stray unresolved conflict marker in ENVIRONMENT.md
A single orphaned "<<<<<<< HEAD" line (no matching =======/>>>>>>>
pair) leaked into release/v3.8.50 via PR #10039's merge-conflict
resolution during this session's serial-merge sweep. Repo-wide sweep
confirms no other stray markers exist. Table structure verified
intact before/after removal.
2026-08-18 12:14:45 -03:00
Diego Rodrigues de Sa e Souza
04af8b1517 feat(compression): adota omniglyph 1.4.0, perfis semânticos e contabilidade com evidência (#10647)
* feat(compression): target-wire OmniGlyph stage and transport fidelity gate

Roda o OmniGlyph depois da tradução para o wire real do provedor, em vez do
corpo de origem. Um cliente OpenAI roteado para Claude deixava de comprimir com
skip:source_format_not_claude porque o corpo ainda estava em formato OpenAI
quando a engine era avaliada.

- dispatch nativo por wire: Anthropic Messages, OpenAI Chat Completions e
  OpenAI Responses (input[] preservado, sem achatar para messages[]);
- estágio target-wire pós-translateRequest, com guarda contra dupla compressão
  no caminho Claude→OpenAI;
- preserveSystemPrompt do OmniRoute mapeado para compressSystem: false;
- imageTransportPolicy: fidelidade de bytes/dimensões separada de supportsVision;
  só Anthropic/Claude tem recibo byte-preserving, o resto é fail-closed;
- contagem de tokens de data URL PNG no wire OpenAI (marcador ;base64,);
- README e i18n en/pt-BR com claims escopados ao caminho medido.

* feat(compression): adota omniglyph 1.4.0 e tira o gate de modelo da env do host

O 1.4.0 introduziu escopos de segurança e passou a resolvê-los dentro de
isOmniGlyphSupportedModel() lendo process.env.OMNIGLYPH_PROFILE. Somado ao
OMNIGLYPH_MODELS que já existia, duas variáveis do ambiente do host decidiam em
silêncio o gate de TODO request do OmniRoute: passthrough desligaria a engine
inteira e OMNIGLYPH_MODELS admitiria modelos sem recibo medido, enquanto a UI
segue prometendo "Claude Fable 5 na rota direta medida".

O adapter passa a usar isOmniGlyphSupportedModelForScope() com escopo explícito
e fixa o escopo mais restrito como teto: a env só pode ESTREITAR a allowlist,
nunca alargar. Os dois wires compartilham a mesma lista no pacote desde o
1.4.0, então uma checagem cobre Anthropic e GPT.

- omniglyph ^1.3.1 -> ^1.4.0 (lock em 1.4.0);
- testes de regressão para os dois caminhos de sequestro por env;
- teste de contrato dos exports novos (escopo, perfis, accounting).

O 1.4.0 também traz, sem mudança de código aqui: correção do glyph K que era
lido como H, remoção do backtracking polinomial no secret-guard, overrides do
pnpm em pnpm-workspace.yaml e as transitivas vulneráveis resolvidas.

* feat(compression): expõe os perfis semânticos do omniglyph nos três wires

O 1.4.0 trouxe perfis nomeados (coding-safe, balanced, aggressive,
passthrough), mas só transformAnthropicMessages() os resolve sozinho: os
transformadores OpenAI recebem TransformOptions cru e ignorariam o campo. Um
perfil escolhido pelo operador valeria no wire Claude e sumiria no OpenAI. O
adapter passa a mesclar o perfil com mergeCompressionProfileOptions() antes de
chamar Chat Completions e Responses.

O default segue aggressive — a política que os recibos publicados mediram.
Medido nesta base: com coding-safe/balanced, uma sessão sem histórico acumulado
para em below_min_chars e a engine não faz nada, porque os dois fixam
minCompressChars no máximo e desligam system/tools/tool-results. Como a engine é
opt-in, um default assim entregaria "ligado, 0% de ganho".

O perfil é TETO, não piso: mergeCompressionProfileOptions não deixa um override
do chamador reabrir uma lane lossy que o perfil fechou. Coberto por teste, por
ser contra-intuitivo.

Também fecha um caminho em que o OmniRoute violaria a própria política: o wire
OpenAI do pacote não tem compressSystem — honra apenas compressTools,
gptHistory, minCompressChars e reflow, e sempre troca a instrução por um
ponteiro para a imagem. Com preserveSystemPrompt ligado, imagear assim queimaria
o prefixo quente que a decisão cache-aware está protegendo, sem nada no corpo
devolvido denunciando. A engine agora pula com
skip:system_preservation_unsupported_on_wire.

* feat(compression): contabilidade física do omniglyph com grau de evidência

O adapter descartava o TransformInfo inteiro, então a UI mostrava um número de
economia sem dizer de onde ele vinha — contagem do provider, estimativa ou só
diferença de bytes. O 1.4.0 expõe normalizeAccounting(), que classifica essa
evidência e resolve a semântica de cache por família: Anthropic reporta input,
cache-create e cache-read em buckets DISJUNTOS, enquanto OpenAI e xAI reportam
cached como SUBCONJUNTO do input. Somar à mão dá double-count silencioso.

O novo omniglyphTelemetry.ts não filtra por denylist — MONTA um objeto novo,
campo a campo, só com número e enum. TransformInfo mistura contadores
inofensivos com material que não pode ser persistido: bytes PNG,
imageSourceText(s), recoverable[].text, os sha8 de system/CLAUDE.md/primeira
mensagem, nomes de tags observadas e o bloco env (cwd, branch, versões). Copiar
o objeto inteiro transformaria telemetria de compressão em vazamento de prompt.
O teste de negação prova que segredo, caminho do operador, texto do system e
base64 não aparecem, e varre a allowlist exigindo que toda string seja de um
enum conhecido.

- provider threaded do chatCore e do bridge Codex WS até a engine; ausente vira
  `unknown`, que faz o upstream recusar adivinhar buckets de cache;
- contabilidade propagada para o engineBreakdown do passo (o agregado do
  pipeline soma todas as engines e não serviria);
- skip não emite contabilidade: zeros ali seriam indistinguíveis de "a engine
  nem rodou".

* feat(compression): perfil do omniglyph configurável, persistido e documentado

Fecha o caminho do operador: o perfil já existia no adapter, mas só como
default de código. Agora atravessa schema Zod, normalizador do banco, API de
settings e a página dedicada do engine.

- OmniglyphConfig tipado + omniglyphConfigSchema (z.enum dos quatro perfis);
- normalizeOmniglyphConfig: nome desconhecido vindo do storage cai para o
  default em vez de virar "roda com a política padrão";
- seletor na página do engine, com PATCH próprio — o perfil vive fora do mapa
  `engines`, e mandá-lo junto reescreveria o mapa inteiro (o store persiste o
  mapa como uma linha JSON só);
- i18n en/pt-BR descrevendo o custo medido de cada perfil, não só o nome;
- README e COMPRESSION_ENGINES.md com a regra do teto e o motivo de o default
  não ser o perfil mais seguro.

Corrige de passagem um teste-irmão que ninguém via: o gate de transporte na UI
deixou de dizer "direct Anthropic" quando os wires OpenAI nativos entraram, mas
tests/unit/ui/omniglyphContextPage.test.tsx continuou afirmando a cópia antiga.
O arquivo inteiro estava excluído do vitest.config.ts como "#8618 pre-existing
failure", então a quebra passou silenciosa. Com a asserção alinhada o arquivo
fecha 3/3, e a exclusão sai — o próprio comentário mandava removê-la quando
corrigida.

A doc não nomeia OMNIGLYPH_MODELS: o gate de docs fabricadas está certo em
apontar que o OmniRoute nunca lê essa env — quem lê é o pacote.

* fix(i18n): paridade do locale vi com as chaves novas do perfil do omniglyph

`tests/unit/i18n-vi-completeness.test.ts` exige paridade ESTRITA de chaves entre
en e vi — diferente do ratchet `i18n:check-ui-coverage`, que passa com 80%. As 11
chaves do seletor de perfil entraram só em en e pt-BR, e o gate de cobertura
seguiu verde, então a quebra só apareceu na matriz completa do CI.

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-18 12:13:48 -03:00
Diego Rodrigues de Sa e Souza
233ac40e9d refactor(sse): ExecutorRegistry — route executor lookup through a runtime registry (R0.3) (#10633)
* test(sse): golden characterization of the executor map before the R0.3 registry refactor

Freezes the 137-entry provider-id → executor mapping (class, provider
identity, backing PROVIDERS config), the no-shared-instances invariant,
and the getExecutor() dispatch rules (memoized DefaultExecutor fallback,
cloud-agent guard #6699, search-provider guard #10274) as stable JSON
snapshots. The upcoming ExecutorRegistry must keep both snapshots
byte-identical.

* refactor(sse): route executor lookup through ExecutorRegistry (R0.3)

Adds open-sse/executors/registry.ts (Map-based registry mirroring
translator/registry.ts): the built-in table in executors/index.ts stays
declarative, every entry is registered at module load, and
getExecutor()/hasSpecializedExecutor() resolve through the registry.
DefaultExecutor fallback, its memoization, and the cloud-agent (#6699) /
search-provider (#10274) guards are unchanged.

Also fixes a latent lookup leak: the old object-literal lookup treated
Object.prototype names (constructor, toString, ...) as specialized
executors; the Map registry resolves them to the DefaultExecutor
fallback like any unknown provider.

Parity proof: executor-map golden (137 entries, byte-identical
before/after), check:known-symbols green, 1018 tests across the 65
executor test files green. Docs: OPEN_SSE_ARCHITECTURE factory section
corrected (it claimed generation from providerRegistry).

Refs #3501

* test(executors): regenerate ExecutorRegistry golden snapshots after release sync

release/v3.8.50 sunset mimocode and added cloudflare-playground + jina-search
since this PR's snapshots were captured; refresh the golden fixtures to match.

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-18 12:08:38 -03:00
Diego Rodrigues de Sa e Souza
397a0e351e fix(compression): gate de estágio não derruba o pipeline com engine sem metadata (#10655)
`assertValidEngine()` valida id, apply, compress, getConfigSchema e
validateConfig — não exige `metadata`. Uma engine registrada sem esse campo é,
portanto, um registro legal. Mas `canRunAtCompressionStage` lia
`engine.metadata.executionStages` sem guarda, então essa engine legal derrubava o
pipeline inteiro com `TypeError: Cannot read properties of undefined` em vez de
falhar aberto, que é o contrato da compressão.

Metadata ausente é o mesmo caso de "não declarou executionStages" e passa a cair
no mesmo fallback documentado: só pre-translation.

Isso destravava também `tests/unit/compression/pipeline-circuit-breaker.test.ts`,
que registra uma engine de teste sem metadata e vinha 8/9 na base — agora 9/9. O
teste novo torna o contrato explícito, em vez de deixá-lo dependendo de uma
reprodução incidental noutro arquivo.

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
2026-08-18 12:06:19 -03:00
s3np4
d094822fc3 fix(deps): upgrade @atjsh/llmlingua-2 to 2.0.5 and drop @tensorflow/tfjs from the LLMLingua SLM stack
@atjsh/llmlingua-2@2.0.5 adds official Transformers.js v4 support (peers
"@huggingface/transformers": "^3.5.2 || ^4.0.0") and 2.0.4+ no longer requires
@tensorflow/tfjs, restoring compatibility with OmniRoute's Transformers.js v4
while removing the largest single contributor to the optional runtime footprint.
Updates the worker gate comments, colocate/standalone/optional-pack scripts,
Docker standalone validation, dependency allowlist, tests, and docs
(COMPRESSION_ENGINES.md, release checklists + i18n). Closes #10536.
2026-08-17 08:09:19 -05:00
136 changed files with 2318 additions and 1809 deletions

View File

@@ -50,13 +50,13 @@ updates:
# bumps; majors here need their own PR and a deliberate migration review.
- dependency-name: "ioredis"
update-types: ["version-update:semver-major"]
# @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN.
# It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/
# compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2)
# and for local memory embeddings (src/lib/memory/embedding/transformersLocal.ts),
# and was VPS-validated at 3.5.2 (#4014). 4.x breaks both, and even 3.x minors must
# be re-validated on the VPS — so freeze ALL auto-bumps (no update-types = ignore
# every version). Migrate it intentionally, not via dependabot (#4050).
# @huggingface/transformers is VPS-validated at ^4.2.0 (migrated intentionally in
# #9962). It is load-bearing for the LLMLingua ONNX compression engine (open-sse/
# services/compression/engines/llmlingua/ — @atjsh/llmlingua-2@2.0.5 peers on
# "@huggingface/transformers": "^3.5.2 || ^4.0.0") and for local memory embeddings
# (src/lib/memory/embedding/transformersLocal.ts). Further majors must be re-validated
# on the VPS — so keep auto-bumps frozen (no update-types = ignore every version).
# Migrate it intentionally, not via dependabot (#4050).
- dependency-name: "@huggingface/transformers"
- package-ecosystem: "github-actions"

View File

@@ -161,7 +161,7 @@ COPY . ./
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-next-cache,target=/app/.build/next/cache \
mkdir -p /app/data \
&& npm run build \
&& node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
&& node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
# ── Runner base ────────────────────────────────────────────────────────────
FROM base AS runner-base

View File

@@ -557,7 +557,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
- **🧠 Memory you control** — off by default, opt-in int8 vector quantization + typed decay, per-request `x-omniroute-no-memory`. → [Memory](docs/frameworks/MEMORY.md)
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite), opt-in credential-masking guardrail (redacts leaked API keys/secrets in both directions), free DuckDuckGo last-resort web search, and an optional OIDC login gate for the dashboard (password login always stays available). → [Guardrails](docs/security/GUARDRAILS.md)
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Google Imagen, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **340-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
@@ -877,7 +877,7 @@ Engines run in pipeline order; each is independently toggleable and configurable
<tr><td align="center" nowrap>9</td><td align="left" nowrap><b>Aggressive</b></td><td align="left">Summarization + progressive aging of old turns</td></tr>
<tr><td align="center" nowrap>10</td><td align="left" nowrap><b>LLMLingua-2</b></td><td align="left">ML semantic pruning via MobileBERT ONNX — code-safe, async</td></tr>
<tr><td align="center" nowrap>11</td><td align="left" nowrap><b>Ultra</b></td><td align="left">Heuristic token pruning with an optional small-model (SLM) tier</td></tr>
<tr><td align="center" nowrap>12</td><td align="left" nowrap><b>OmniGlyph</b></td><td align="left">Experimental context-as-image encoding routed to Claude Fable 5 (most aggressive; opt-in)</td></tr>
<tr><td align="center" nowrap>12</td><td align="left" nowrap><b>OmniGlyph</b></td><td align="left">Experimental context-as-image encoding for measured Claude Fable 5 on the direct Anthropic wire; GPT 5.6 transformers remain fail-closed pending provider receipts. Four compression profiles (aggressive default, balanced, coding-safe, passthrough) (most aggressive; opt-in)</td></tr>
</table>
Code blocks, URLs and structured data are **always preserved** byte-perfect. **One-click presets** combine the engines:

View File

@@ -0,0 +1 @@
- **fix(deps):** upgrade `@atjsh/llmlingua-2` from 2.0.3 to 2.0.5 and remove `@tensorflow/tfjs` from the LLMLingua SLM stack — 2.0.5 adds official Transformers.js v4 support (peers `@huggingface/transformers` at `^3.5.2 || ^4.0.0`) and 2.0.4+ no longer requires TensorFlow.js, restoring compatibility with OmniRoute's Transformers.js v4 while dropping the largest single contributor to the optional runtime footprint ([#10536](https://github.com/diegosouzapw/OmniRoute/issues/10536))

View File

@@ -20,7 +20,6 @@
"@stryker-mutator/tap-runner",
"@swc/helpers",
"@tailwindcss/postcss",
"@tensorflow/tfjs",
"@testing-library/jest-dom",
"@testing-library/react",
"@toon-format/toon",

View File

@@ -19,8 +19,36 @@ OmniRoute compression is built around engine contracts. A mode can run one engin
| `aggressive` | Caveman + history/tool summarizers | Long chat sessions |
| `ultra` | Caveman + pruning helpers | Context-limit recovery |
| `rtk` | RTK | Terminal, shell, build, test, and git output |
| `omniglyph` | OmniGlyph | Context-as-image on the native provider wire |
| `stacked` | Pipeline, default `rtk -> caveman` | Mixed tool logs and prose, max savings |
### OmniGlyph compression profiles
The `omniglyph` engine (package `omniglyph`, 1.4.0+) accepts a named semantic profile, set
globally through `omniglyph.profile` in the compression settings or per step through the
stacked pipeline's step config:
| Profile | Boundary |
| ------------- | ------------------------------------------------------------------------------------------------ |
| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history |
| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history |
| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns |
| `passthrough` | Routes without transforming; the engine is skipped |
The profile is a **ceiling, not a floor**: `mergeCompressionProfileOptions` in the package
refuses to let a caller override reopen a lossy lane the profile closed, so a per-step
`preserveSystemPrompt: false` cannot re-enable system compression under `coding-safe`.
Measured on this codebase: `coding-safe` and `balanced` raise `minCompressChars` to its
maximum and keep system, tool schemas and tool results native, so a session that has not
accumulated history yet stops at `below_min_chars` and the engine transforms nothing. That
is why the default is `aggressive` rather than the safest profile.
The package resolves its own model scope and profile from its environment configuration.
OmniRoute never delegates the decision: the adapter pins the model gate to the package's
most restrictive scope, so host environment settings can only narrow the allowlist, never
widen it past OmniRoute's measured receipts.
## Engine Registry
The registry lives in `open-sse/services/compression/engines/registry.ts`. Engines expose a shared
@@ -142,22 +170,22 @@ override points it at a local copy instead (offline / air-gapped installs).
### Optional dependencies & on-demand install
The prunable LLMLingua runtime peer stack is **optional**. Three packages are declared as
The prunable LLMLingua runtime peer stack is **optional**. Two packages are declared as
`optionalDependencies` in `package.json` and kept **external** by the production build
(`scripts/build/prepublish.ts` does not bundle them):
| Package | Version (pin) | Notes |
| -------------------- | ------------- | ---------------------------------------------- |
| `@atjsh/llmlingua-2` | `2.0.3` | Entry package; declares the others as peers |
| `@tensorflow/tfjs` | `4.22.0` | Heaviest dep — dominates the ~800 MB footprint |
| `js-tiktoken` | `^1.0.20` | Tokenizer |
| Package | Version (pin) | Notes |
| -------------------- | ------------- | ------------------------------------------- |
| `@atjsh/llmlingua-2` | `2.0.5` | Entry package; declares the others as peers |
| `js-tiktoken` | `^1.0.20` | Tokenizer |
`@huggingface/transformers` is pinned at `3.5.2` as an **optional** dependency (shared with
the local embeddings path and also traced into the standalone bundle). Keeping it optional prevents
`onnxruntime-node` CUDA provider postinstall failures on CUDA 11 hosts from aborting the whole
OmniRoute install; when the optional stack is absent, LLMLingua still fail-opens. Only the three
packages above are prunable SLM peers. A standard `npm install` (dev) installs the optional stack
automatically unless optional dependencies are omitted.
`@huggingface/transformers` is pinned at `^4.2.0` (shared with the local embeddings path and
also traced into the standalone bundle); `@atjsh/llmlingua-2@2.0.5` peers on it with
`"^3.5.2 || ^4.0.0"`, so both Transformers.js v3 and v4 are supported. Since 2.0.4,
`@atjsh/llmlingua-2` no longer requires `@tensorflow/tfjs`, which removed the largest single
contributor (TensorFlow.js) from the SLM stack. Only the two packages above are prunable SLM
peers. A standard `npm install` (dev) installs the optional stack automatically unless optional
dependencies are omitted.
**Why on-demand:** the npm-published package, the standalone bundle, and the Docker image
ship **without** these deps to stay slim. When they are absent, the worker's dependency
@@ -167,11 +195,12 @@ error logged). To activate it in a pruned environment, install the optional stac
```bash
# pin to the versions declared in package.json optionalDependencies
npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken
npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken
```
Roughly **~800 MB** total: the TensorFlow.js + transformers runtimes dominate; the
TinyBERT model adds ~57 MB downloaded at first use (not via npm).
The `@tensorflow/tfjs` removal (2.0.4+) eliminates the previously dominant ~800 MB
contributor — the remaining footprint is the transformers.js + onnxruntime-node runtimes,
plus the TinyBERT model (~57 MB) downloaded at first use (not via npm).
Per environment:

View File

@@ -142,22 +142,22 @@ wskazuje zamiast tego lokalną kopię (instalacje offline / air-gapped).
### Opcjonalne zależności i instalacja on-demand
Przycinany stos peerów runtime LLMLingua jest **opcjonalny**. Trzy pakiety są zadeklarowane jako
Przycinany stos peerów runtime LLMLingua jest **opcjonalny**. Dwa pakiety są zadeklarowane jako
`optionalDependencies` w `package.json` i utrzymywane jako **external** przez build produkcyjny
(`scripts/build/prepublish.ts` ich nie bundluje):
| Package | Version (pin) | Notes |
| -------------------- | ------------- | ------------------------------------------------- |
| `@atjsh/llmlingua-2` | `2.0.3` | Pakiet wejściowy; deklaruje pozostałe jako peery |
| `@tensorflow/tfjs` | `4.22.0` | Najcięższa zależność — dominuje footprint ~800 MB |
| `js-tiktoken` | `^1.0.20` | Tokenizer |
| Package | Version (pin) | Notes |
| -------------------- | ------------- | ------------------------------------------- |
| `@atjsh/llmlingua-2` | `2.0.5` | Pakiet wejściowy; deklaruje pozostałe jako peery |
| `js-tiktoken` | `^1.0.20` | Tokenizer |
`@huggingface/transformers` jest pinowany na `3.5.2` jako **opcjonalna** zależność (współdzielona ze
ścieżką lokalnych embeddings i również śledzona do standalone bundle). Utrzymanie jej jako optional
zapobiega awariom postinstall providera CUDA `onnxruntime-node` na hostach CUDA 11, które przerywałyby
całą instalację OmniRoute; gdy opcjonalny stos jest nieobecny, LLMLingua nadal fail-openuje. Tylko trzy
powyższe pakiety to przycinane peery SLM. Standardowe `npm install` (dev) instaluje opcjonalny stos
automatycznie, o ile opcjonalne zależności nie zostaną pominięte.
`@huggingface/transformers` jest pinowany na `^4.2.0` (współdzielony ze ścieżką lokalnych embeddings
i również śledzony do standalone bundle); `@atjsh/llmlingua-2@2.0.5` peeruje na nim przez
`"^3.5.2 || ^4.0.0"`, więc obsługiwane są zarówno Transformers.js v3, jak i v4. Od 2.0.4
`@atjsh/llmlingua-2` nie wymaga już `@tensorflow/tfjs`, co usunęło największy pojedynczy wkład
(TensorFlow.js) ze stosu SLM. Tylko dwa powyższe pakiety to przycinane peery SLM. Standardowe
`npm install` (dev) instaluje opcjonalny stos automatycznie, o ile opcjonalne zależności nie zostaną
pominięte.
**Dlaczego on-demand:** pakiet publikowany w npm, standalone bundle i obraz Docker
dostarczane są **bez** tych zależności, aby pozostać lekkie. Gdy ich brakuje, bramka zależności
@@ -167,11 +167,12 @@ logowanego błędu). Aby aktywować go w przyciętym środowisku, zainstaluj opc
```bash
# pin to the versions declared in package.json optionalDependencies
npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken
npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken
```
Łącznie mniej więcej **~800 MB**: dominują runtimey TensorFlow.js + transformers; model
TinyBERT dodaje ~57 MB pobierane przy pierwszym użyciu (nie przez npm).
Usunięcie `@tensorflow/tfjs` (2.0.4+) eliminuje wcześniej dominujący wkład ~800 MB — pozostały
footprint to runtimey transformers.js + onnxruntime-node oraz model TinyBERT (~57 MB) pobierany
przy pierwszym użyciu (nie przez npm).
Per środowisko:

View File

@@ -326,13 +326,11 @@ Przed wypuszczeniem dowolnego wydania v3.8.x zweryfikuj te dodatkowe pozycje:
- [ ] `npm install -g omniroute@<this-version>` uruchamia postinstall bez fatalnego wyjścia
- [ ] Ścieżka update zachowuje optional deps: `omniroute update --apply` i auto-updater
uruchamiają `npm install -g … --include=optional`, żeby `optionalDependencies` (better-sqlite3,
keytar, tls-client oraz stack SLM llmlingua: `@atjsh/llmlingua-2`,
`@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) przeżyły update.
`@huggingface/transformers` zostaje optional, żeby jego postinstall providera CUDA `onnxruntime-node`
nie mógł przerwać instalacji na hostach CUDA 11. Tier ultra `modelPath` SLM potrzebuje też
keytar, tls-client oraz stack SLM llmlingua: `@atjsh/llmlingua-2@2.0.5`,
`js-tiktoken`) przeżyły update. Tier ultra `modelPath` SLM potrzebuje też
modelu tinybert, auto-pobieranego do `${DATA_DIR}/models/llmlingua` przy pierwszym użyciu. Postinstall
(`scripts/build/colocateOptionals.mjs`) następnie ko-lokuje opcjonalne zamknięcie SLM do
`dist/node_modules`, żeby worker rozwiązywał JEDNĄ opcjonalną instancję `@huggingface/transformers` 3.5.2
`dist/node_modules`, żeby worker rozwiązywał JEDNĄ instancję `@huggingface/transformers` ^4.2.0
— standalone trace bundluje tylko transformers, nie dynamicznie importowane
optionals, więc bez tego worker załadowałby llmlingua-2 przeciw transformers z roota
i tier SLM cicho fail-openowałby.

View File

@@ -275,14 +275,12 @@ npm run build:release
- [ ] `npm install -g omniroute@<this-version>` 运行 postinstall 无致命退出
- [ ] 更新路径保留可选依赖:`omniroute update --apply` 以及自动更新器
运行 `npm install -g … --include=optional` 以确保 `optionalDependencies`better-sqlite3、
keytar、tls-client 以及 llmlingua SLM 栈:`@atjsh/llmlingua-2`、
`@huggingface/transformers@3.5.2`、`@tensorflow/tfjs`、`js-tiktoken`)在更新后仍然存在。
`@huggingface/transformers` 保持为可选依赖,这样其 `onnxruntime-node` CUDA provider postinstall
不会在 CUDA 11 主机上中断安装。Ultra 模式的 `modelPath` SLM 层还需要
keytar、tls-client 以及 llmlingua SLM 栈:`@atjsh/llmlingua-2@2.0.5`、
`js-tiktoken`)在更新后仍然存在。Ultra 模式的 `modelPath` SLM 层还需要
tinybert 模型,首次使用时自动下载到 `${DATA_DIR}/models/llmlingua`。postinstall
`scripts/build/colocateOptionals.mjs`)随后将 SLM 可选依赖闭包共置到
`dist/node_modules`,使 Worker 解析单一的 `@huggingface/transformers` 3.5.2
可选实例 — standalone trace 仅打包 transformers不包含动态导入的
`dist/node_modules`,使 Worker 解析单一的 `@huggingface/transformers` ^4.2.0
实例 — standalone trace 仅打包 transformers不包含动态导入的
可选依赖,否则 Worker 会基于根目录的 transformers 加载 llmlingua-2
SLM 层将静默失效。
- [ ] `omniroute status` 在无 `.env` 的情况下正常工作CLI Token 路径,仅 loopback

View File

@@ -322,14 +322,12 @@ npm run build:release
- [ ] `npm install -g omniroute@<此版本>` 執行 postinstall 而不會致命退出
- [ ] 更新路徑保留選擇性依賴:`omniroute update --apply` 和自動更新器
執行 `npm install -g … --include=optional`,因此 `optionalDependencies`better-sqlite3、
keytar、tls-client以及 llmlingua SLM 堆疊:`@atjsh/llmlingua-2`、
`@huggingface/transformers@3.5.2`、`@tensorflow/tfjs`、`js-tiktoken`)在更新後仍會保留。
`@huggingface/transformers` 維持選擇性,因此其 `onnxruntime-node` CUDA 提供者的 postinstall
不會在 CUDA 11 主機上中斷安裝。Ultra `modelPath` SLM 層還需要
keytar、tls-client以及 llmlingua SLM 堆疊:`@atjsh/llmlingua-2@2.0.5`、
`js-tiktoken`)在更新後仍會保留。Ultra `modelPath` SLM 層還需要
tinybert 模型,會在首次使用時自動下載到 `${DATA_DIR}/models/llmlingua`。Postinstall
`scripts/build/colocateOptionals.mjs`)接著將 SLM 選擇性閉包複製到
`dist/node_modules`,使工作者解析到**單一** `@huggingface/transformers` 3.5.2
選擇性實例——獨立追蹤僅捆綁 transformers而非動態匯入的
`dist/node_modules`,使工作者解析到**單一** `@huggingface/transformers` ^4.2.0
實例——獨立追蹤僅捆綁 transformers而非動態匯入的
選擇性套件,因此若無此步驟,工作者會載入 llmlingua-2 並使用根目錄的 transformers
導致 SLM 層靜默地失敗但仍保持運作。
- [ ] `omniroute status` 在無 `.env` 的情況下正常運作(僅限 CLI 權杖路徑,迴環介面)

View File

@@ -342,14 +342,12 @@ Before shipping any v3.8.x release, verify these additional items:
- [ ] `npm install -g omniroute@<this-version>` runs postinstall without fatal exit
- [ ] Update path keeps optional deps: `omniroute update --apply` and the auto-updater
run `npm install -g … --include=optional` so `optionalDependencies` (better-sqlite3,
keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2`,
`@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) survive an update.
`@huggingface/transformers` stays optional so its `onnxruntime-node` CUDA provider postinstall
cannot abort installation on CUDA 11 hosts. The ultra `modelPath` SLM tier also needs the
keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2@2.0.5`,
`js-tiktoken`) survive an update. The ultra `modelPath` SLM tier also needs the
tinybert model, auto-downloaded to `${DATA_DIR}/models/llmlingua` on first use. Postinstall
(`scripts/build/colocateOptionals.mjs`) then co-locates the SLM optional closure into
`dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` 3.5.2
optional instance — the standalone trace bundles only transformers, not the dynamically-imported
`dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` ^4.2.0
instance — the standalone trace bundles only transformers, not the dynamically-imported
optionals, so without this the worker would load llmlingua-2 against the root's transformers
and the SLM tier would silently fail-open.
- [ ] `omniroute status` works with no `.env` (CLI token path, loopback only)

View File

@@ -1520,7 +1520,6 @@ These settings were introduced after the previous environment-contract snapshot.
| Variable | Default | Source File | Description |
| --- | --- | --- | --- |
| `OMNIROUTE_CHAT_ADMISSION_QUEUE_MS` | `2000` | `src/shared/middleware/chatBodyAdmission.ts` | Maximum wait for a heavyweight chat admission slot before a retryable `503`; a short bounded wait serializes agent bursts instead of an instant `503`. `0` restores immediate rejection. |
<<<<<<< HEAD
| `OMNIROUTE_CHAT_ADMISSION_MAX_QUEUED_BYTES` | `4194304` (4 MB) | `src/shared/middleware/chatBodyAdmission.ts` | Queued-bytes budget for the admission wait: bounds total buffered body bytes parked process-wide so the wait cannot amplify the heap (#4380). Over-budget waits receive a retryable `503` immediately. |
| `OMNIROUTE_CHAT_VIRTUAL_TTL_MS` | `60000` (60 s) | `src/shared/middleware/chatBodyAdmission.ts` | Deprecated no-op since #10110: per-session admission lanes were removed in favor of one process-wide budget. Accepted for configuration compatibility; ignored. |
| `OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS` | `64` | `src/shared/middleware/chatBodyAdmission.ts` | Deprecated no-op since #10110: per-session admission lanes were removed in favor of one process-wide budget. Accepted for configuration compatibility; ignored. |

View File

@@ -2,8 +2,8 @@
//
// These models are pinned from the live `:fetchAvailableModels` endpoint
// (https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels) using a
// real `agy` consumer-OAuth token. The public catalog exposes the upstream Gemini 3.6
// and 3.5 Flash ids verbatim; the shared Antigravity executor dispatches them unchanged.
// real `agy` consumer-OAuth token. The public catalog exposes the upstream Gemini 3.7
// Flash ids verbatim; the shared Antigravity executor dispatches them unchanged.
//
// The `agy` provider reuses the `antigravity` executor/translator (identical backend),
// but keeps its own catalog so the CLI and IDE model surfaces can evolve independently.
@@ -12,11 +12,11 @@
// they are not chat-callable.
export const AGY_PUBLIC_MODELS = Object.freeze([
// Gemini 3.6 Flash tiers. The live endpoint selects High by default and advertises
// all three ids to both the IDE 2.1.1 and CLI 1.1.x clients.
// Gemini 3.7 Flash tiers. The live endpoint selects High by default and advertises
// all three ids to both the IDE 2.5.5 and CLI 1.1.x clients.
{
id: "gemini-3.6-flash-high",
name: "Gemini 3.6 Flash (High)",
id: "gemini-3.7-flash-high",
name: "Gemini 3.7 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
@@ -24,8 +24,8 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
toolCalling: true,
},
{
id: "gemini-3.6-flash-medium",
name: "Gemini 3.6 Flash (Medium)",
id: "gemini-3.7-flash-medium",
name: "Gemini 3.7 Flash (Medium)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
@@ -33,27 +33,8 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
toolCalling: true,
},
{
id: "gemini-3.6-flash-low",
name: "Gemini 3.6 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// Claude (Antigravity backend).
{
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Thinking)",
id: "gemini-3.7-flash-low",
name: "Gemini 3.7 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
@@ -79,45 +60,6 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3-flash-agent",
name: "Gemini 3.5 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.5-flash-low",
name: "Gemini 3.5 Flash (Medium)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.5-flash-extra-low",
name: "Gemini 3.5 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// Gemini 3.7 Flash: single callable public model (upstream exposes only
// gemini-3.7-flash-tiered; suffixed tier ids 404). One entry so it does not
// collide under the #3696 public-id uniqueness invariant.
{
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.1-flash-lite",
name: "Gemini 3.1 Flash Lite",
@@ -125,27 +67,23 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
maxOutputTokens: 65535,
toolCalling: true,
},
// Gemini 2.5
// Claude (Antigravity backend).
{
id: "gemini-2.5-flash-thinking",
name: "Gemini 2.5 Flash Thinking",
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 1048576,
maxOutputTokens: 65535,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Thinking)",
contextLength: 1048576,
maxOutputTokens: 65535,
toolCalling: true,
},
{
id: "gemini-2.5-flash-lite",
name: "Gemini 2.5 Flash Lite",
contextLength: 1048576,
maxOutputTokens: 65535,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// GPT-OSS
@@ -161,6 +99,21 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
const AGY_PUBLIC_MODEL_IDS = new Set(AGY_PUBLIC_MODELS.map((model) => model.id));
const AGY_NON_CHAT_MODEL_IDS = new Set(["tab_flash_lite_preview", "tab_jump_flash_lite_preview"]);
const AGY_RETIRED_MODEL_IDS = new Set([
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-high",
"gemini-3.5-flash-medium",
"gemini-3.5-flash-preview",
"gemini-2.5-pro",
"gemini-2.5-flash-thinking",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
]);
const AGY_CLIENT_VISIBLE_MODEL_NAMES = Object.freeze(
AGY_PUBLIC_MODELS.reduce<Record<string, string>>((acc, model) => {
@@ -178,5 +131,5 @@ export function isUserCallableAgyModelId(modelId: string): boolean {
}
export function isDiscoverableAgyModelId(modelId: string): boolean {
return !!modelId && !AGY_NON_CHAT_MODEL_IDS.has(modelId);
return !!modelId && !AGY_NON_CHAT_MODEL_IDS.has(modelId) && !AGY_RETIRED_MODEL_IDS.has(modelId);
}

View File

@@ -1,7 +1,7 @@
export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
// Gemini 3.7 Flash tiers listed by the current official Antigravity model catalog
// alongside the existing Gemini 3.6 tiers. Keep the upstream model ids unchanged so
// discovery and execution address the same models selected by the native client.
// Gemini 3.7 Flash tiers listed by the current official Antigravity model catalog.
// Keep the upstream model ids unchanged so discovery and execution address the same
// models selected by the native client.
{
id: "gemini-3.7-flash-high",
name: "Gemini 3.7 Flash (High)",
@@ -20,51 +20,9 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
// Gemini 3.6 Flash tiers retained alongside the newer Gemini 3.7 tiers.
{
id: "gemini-3.6-flash-high",
name: "Gemini 3.6 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.6-flash-medium",
name: "Gemini 3.6 Flash (Medium)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.6-flash-low",
name: "Gemini 3.6 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// Claude (Antigravity backend). The `agy` provider already ships these from the live
// :fetchAvailableModels probe (see agyModels.ts) and discussion #3184 confirmed they
// are user-callable through the `antigravity` OAuth provider too — same backend.
// `antigravity/claude-opus-4-6-thinking` and `antigravity/claude-sonnet-4-6` both work.
// They are upstream IDs, so no alias remapping is required.
{
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Thinking)",
id: "gemini-3.7-flash-low",
name: "Gemini 3.7 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
@@ -92,50 +50,6 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
// Gemini 3.5 Flash tiers exposed by Antigravity's model selector. Public ids match
// fetchAvailableModels and are forwarded upstream unchanged:
// High -> gemini-3-flash-agent (displayName: Gemini 3.5 Flash (High))
// Medium -> gemini-3.5-flash-low (displayName: Gemini 3.5 Flash (Medium))
// Low -> gemini-3.5-flash-extra-low (displayName: Gemini 3.5 Flash (Low))
{
id: "gemini-3-flash-agent",
name: "Gemini 3.5 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.5-flash-low",
name: "Gemini 3.5 Flash (Medium)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.5-flash-extra-low",
name: "Gemini 3.5 Flash (Low)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// Gemini 3.7 Flash: Antigravity's live catalog exposes a single upstream id
// gemini-3.7-flash-tiered; the suffixed tier ids 404 upstream. Kept as one
// callable public model so it does not collide with the #3696 uniqueness invariant.
{
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.1-flash-lite",
name: "Gemini 3.1 Flash Lite",
@@ -143,25 +57,27 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
maxOutputTokens: 65535,
toolCalling: true,
},
// Claude (Antigravity backend). The `agy` provider already ships these from the live
// :fetchAvailableModels probe (see agyModels.ts) and discussion #3184 confirmed they
// are user-callable through the `antigravity` OAuth provider too — same backend.
// `antigravity/claude-opus-4-6-thinking` and `antigravity/claude-sonnet-4-6` both work.
// They are upstream IDs, so no alias remapping is required.
{
id: "gemini-2.5-flash-thinking",
name: "Gemini 2.5 Flash Thinking",
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 1048576,
maxOutputTokens: 65535,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Thinking)",
contextLength: 1048576,
maxOutputTokens: 65535,
toolCalling: true,
},
{
id: "gemini-2.5-flash-lite",
name: "Gemini 2.5 Flash Lite",
contextLength: 1048576,
maxOutputTokens: 65535,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
{
@@ -175,12 +91,6 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
]);
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
// Gemini 3.7 Flash: the live catalog (fetchAvailableModels on daily-cloudcode-pa)
// exposes a single upstream id `gemini-3.7-flash-tiered`; the agy CLI maps all
// display tiers (high/medium/low) to it. Verified 200 OK with thinking_level and
// thinkingBudget configs. The suffixed ids 404 upstream ("Requested entity was not found").
// Exposed as ONE callable model (see #3696: public ids must be unique upstream ids).
"gemini-3.7-flash": "gemini-3.7-flash-tiered",
// gemini-3.1-pro-low is not aliased: the upstream accepts it verbatim.
// gemini-3.1-pro-high: the discovery slot returns HTTP 400 on v1internal;
// the live upstream id is gemini-pro-agent (see ANTIGRAVITY_PUBLIC_MODELS).
@@ -249,10 +159,19 @@ const ANTIGRAVITY_NON_CHAT_MODEL_IDS = new Set([
const ANTIGRAVITY_RETIRED_MODEL_IDS = new Set([
"gemini-3-pro-preview",
"gemini-3.1-pro",
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-high",
"gemini-3.5-flash-medium",
"gemini-3.5-flash-preview",
"gemini-2.5-pro",
"gemini-2.5-flash-thinking",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-2.5-computer-use-preview-10-2025",
]);
@@ -281,7 +200,12 @@ const ANTIGRAVITY_DROPPED_QUOTA_BUCKETS = new Set<string>([
*/
export function toClientAntigravityQuotaModelId(modelId: string): string | null {
if (!modelId) return null;
if (ANTIGRAVITY_DROPPED_QUOTA_BUCKETS.has(modelId)) return null;
if (
ANTIGRAVITY_DROPPED_QUOTA_BUCKETS.has(modelId) ||
ANTIGRAVITY_RETIRED_MODEL_IDS.has(modelId)
) {
return null;
}
return toClientAntigravityModelId(modelId);
}

View File

@@ -16,27 +16,20 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
* rewrites file timestamps on every deploy, which would report a months-old
* catalog as "updated today". Bump this whenever the entries below change.
*/
export const FREE_CATALOG_CURATED_AT = "2026-07-22";
export const FREE_CATALOG_CURATED_AT = "2026-08-16";
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agy", modelId: "gemini-3.7-flash-high", displayName: "Gemini 3.7 Flash (High)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.7-flash-medium", displayName: "Gemini 3.7 Flash (Medium)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.7-flash-low", displayName: "Gemini 3.7 Flash (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-pro-agent", displayName: "Gemini 3.1 Pro (High)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.1-pro-low", displayName: "Gemini 3.1 Pro (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.1-flash-lite", displayName: "Gemini 3.1 Flash Lite", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "claude-opus-4-6-thinking", displayName: "Claude Opus 4.6 (Thinking)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6 (Thinking)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.1-pro-low", displayName: "Gemini 3.1 Pro (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-pro-agent", displayName: "Gemini 3.1 Pro (High)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.7-flash", displayName: "Gemini 3.7 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.6-flash-high", displayName: "Gemini 3.6 Flash (High)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.6-flash-medium", displayName: "Gemini 3.6 Flash (Medium)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.6-flash-low", displayName: "Gemini 3.6 Flash (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3-flash-agent", displayName: "Gemini 3.5 Flash (High)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.5-flash-low", displayName: "Gemini 3.5 Flash (Medium)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.5-flash-extra-low", displayName: "Gemini 3.5 Flash (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.1-flash-lite", displayName: "Gemini 3.1 Flash Lite", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-2.5-flash-thinking", displayName: "Gemini 2.5 Flash Thinking", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash Lite", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gpt-oss-120b-medium", displayName: "GPT-OSS 120B (Medium)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "ai21", modelId: "jamba-large-1.7", displayName: "jamba-large-1.7", monthlyTokens: 0, creditTokens: 10000000, freeType: "one-time-initial", poolKey: "ai21", tos: "avoid" },
{ provider: "ai21", modelId: "jamba-mini-2", displayName: "jamba-mini-2", monthlyTokens: 0, creditTokens: 10000000, freeType: "one-time-initial", poolKey: "ai21", tos: "avoid" },
@@ -188,7 +181,6 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "gemini", modelId: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash-Lite", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" },
{ provider: "gemini", modelId: "gemini-3-flash-preview", displayName: "Gemini 3 Flash Preview", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" },
{ provider: "gemini", modelId: "gemini-3.1-flash-lite", displayName: "Gemini 3.1 Flash-Lite", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" },
{ provider: "gemini", modelId: "gemini-3.5-flash", displayName: "Gemini 3.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" },
{ provider: "glm-cn", modelId: "glm-4-flash", displayName: "GLM-4-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" },
{ provider: "glm-cn", modelId: "glm-4.5-flash", displayName: "GLM-4.5-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" },
{ provider: "glm-cn", modelId: "glm-4.7-flash", displayName: "GLM-4.7-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" },
@@ -369,7 +361,7 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "qoder", modelId: "deepseek-v4-pro", displayName: "DeepSeek-V4-Pro", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" },
{ provider: "qoder", modelId: "deepseek-v4-flash", displayName: "DeepSeek-V4-Flash", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" },
{ provider: "qoder", modelId: "minimax-m3", displayName: "MiniMax-M3", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" },
{ provider: "qwen-web", modelId: "qwen3.8-max", displayName: "Qwen3.8 Max", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" },
{ provider: "qwen-web", modelId: "qwen3.8-max-preview", displayName: "Qwen3.8 Max Preview", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" },
{ provider: "qwen-web", modelId: "qwen3.7-max", displayName: "Qwen3.7 Max", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" },
{ provider: "qwen-web", modelId: "qwen3.7-plus", displayName: "Qwen3.7 Plus", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" },
{ provider: "qwen-web", modelId: "qwen3.6-plus", displayName: "Qwen3.6 Plus", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" },

View File

@@ -5,9 +5,6 @@
"gemini-2-flash-lite": { "rpm": 0, "rpd": 0, "tpm": 0 },
"gemini-2.5-flash-tts": { "rpm": 3, "rpd": 10, "tpm": 10000 },
"gemini-2.5-pro-tts": { "rpm": 0, "rpd": 0, "tpm": 0 },
"imagen-4-generate": { "rpm": -1, "rpd": 25, "tpm": -1 },
"imagen-4-ultra-generate": { "rpm": -1, "rpd": 25, "tpm": -1 },
"imagen-4-fast-generate": { "rpm": -1, "rpd": 25, "tpm": -1 },
"gemma-4-26b-it": { "rpm": 16000, "rpd": 14400, "tpm": 16000 },
"gemma-4-31b-it": { "rpm": 16000, "rpd": 14400, "tpm": 16000 },
"gemini-embedding-exp-03-07": { "rpm": 100, "rpd": 1000, "tpm": 30000 },

View File

@@ -10,7 +10,6 @@ import { SEGMIND_IMAGE_PROVIDER } from "./providers/registry/segmind/imageModels
import { KIE_IMAGE_MODELS } from "./providers/registry/kie/imageModels.ts";
import { FREEPIK_IMAGE_PROVIDER } from "./providers/registry/freepik/index.ts";
import { STABILITY_AI_IMAGE_MODELS } from "./providers/registry/stability-ai/imageModels.ts";
import { GEMINI_IMAGEN_PROVIDER } from "./providers/registry/gemini/imageModels.ts";
import { CHEAPERINFERENCE_IMAGE_PROVIDER } from "./providers/registry/cheaperinference/imageModels.ts";
import {
ADOBE_FIREFLY_IMAGE_ROUTING_ALIASES,
@@ -378,10 +377,6 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024"],
},
// Google AI Studio Imagen family — dedicated :predict endpoint, not generateContent.
// See providers/registry/gemini/imageModels.ts for the full rationale.
gemini: GEMINI_IMAGEN_PROVIDER,
//Curruntly no models serving
nebius: {
id: "nebius",

View File

@@ -107,7 +107,12 @@ export const CHEAPERINFERENCE_MODELS: RegistryModel[] = [
supportsReasoning: true,
toolCalling: true,
},
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true, toolCalling: true },
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
supportsReasoning: true,
toolCalling: true,
},
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true, toolCalling: true },
{
id: "gemini-2.5-flash",
@@ -117,8 +122,8 @@ export const CHEAPERINFERENCE_MODELS: RegistryModel[] = [
toolCalling: true,
},
{
id: "gemini-3-5-flash",
name: "Gemini 3.5 Flash",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,

View File

@@ -140,22 +140,19 @@ export const cursorProvider: RegistryEntry = {
{ id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet Medium" },
{ id: "claude-4.6-sonnet-medium-thinking", name: "Claude 4.6 Sonnet Medium Thinking" },
//
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet" },
{ id: "claude-4.5-sonnet-thinking", name: "Claude 4.5 Sonnet Thinking" },
//
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
//
{ id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
//
{ id: "grok-4.3", name: "Grok 4.3" },
{ id: "grok-4.6-medium", name: "Grok 4.6 Medium" },
{ id: "grok-4.6-fast-medium", name: "Grok 4.6 Fast Medium" },
{ id: "grok-4.6-high", name: "Grok 4.6 High" },
{ id: "grok-4.6-fast-high", name: "Grok 4.6 Fast High" },
{ id: "grok-4.6-xhigh", name: "Grok 4.6 XHigh" },
{ id: "grok-4.6-fast-xhigh", name: "Grok 4.6 Fast XHigh" },
//
{ id: "grok-4.5-medium", name: "Grok 4.5 Medium" },
{ id: "grok-4.5-fast-medium", name: "Grok 4.5 Fast Medium" },
{ id: "grok-4.5-high", name: "Grok 4.5 High" },
{ id: "grok-4.5-fast-high", name: "Grok 4.5 Fast High" },
{ id: "grok-4.5-xhigh", name: "Grok 4.5 XHigh" },
{ id: "grok-4.5-fast-xhigh", name: "Grok 4.5 Fast XHigh" },
//
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k3", name: "Kimi K3" },
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
],
};

View File

@@ -84,14 +84,10 @@ export const DEVIN_MODEL_CATALOG: RegistryModel[] = [
// Gemini
{ id: "gemini-3-1-pro-high", name: "Gemini 3.1 Pro High", contextLength: 1048576 },
{ id: "gemini-3-1-pro-low", name: "Gemini 3.1 Pro Low", contextLength: 1048576 },
{ id: "gemini-3-6-flash-high", name: "Gemini 3.6 Flash High" },
{ id: "gemini-3-6-flash-medium", name: "Gemini 3.6 Flash Medium" },
{ id: "gemini-3-6-flash-low", name: "Gemini 3.6 Flash Low" },
{ id: "gemini-3-6-flash-minimal", name: "Gemini 3.6 Flash Minimal" },
{ id: "gemini-3-5-flash-high", name: "Gemini 3.5 Flash High", contextLength: 1048576 },
{ id: "gemini-3-5-flash-medium", name: "Gemini 3.5 Flash Medium", contextLength: 1048576 },
{ id: "gemini-3-5-flash-low", name: "Gemini 3.5 Flash Low", contextLength: 1048576 },
{ id: "gemini-3-5-flash-minimal", name: "Gemini 3.5 Flash Minimal", contextLength: 1048576 },
{ id: "gemini-3-7-flash-high", name: "Gemini 3.7 Flash High" },
{ id: "gemini-3-7-flash-medium", name: "Gemini 3.7 Flash Medium" },
{ id: "gemini-3-7-flash-low", name: "Gemini 3.7 Flash Low" },
{ id: "gemini-3-7-flash-minimal", name: "Gemini 3.7 Flash Minimal" },
// Grok
{ id: "grok-4-5-high", name: "Grok 4.5 High", contextLength: 500000 },
{ id: "grok-4-5-medium", name: "Grok 4.5 Medium", contextLength: 500000 },

View File

@@ -1,32 +0,0 @@
/**
* Google AI Studio (Gemini API) Imagen family image-generation provider entry.
*
* Uses the dedicated `:predict` endpoint (handled by format "google-imagen"), NOT
* generateContent — so only imagen-* models belong here; gemini flash-image /
* nano-banana route through /v1/chat/completions instead. The models are also
* surfaced live via ListModels; this seed makes them addressable on
* /v1/images/generations. Note: Imagen requires a billing-enabled Google project —
* free-tier keys get 403 / quota 0. The handler builds `{baseUrl}/{model}:predict`.
*
* Extracted out of imageRegistry.ts (which sits right at the 800-line file-size
* cap) so the catalog lives in its own semantic family module, following the same
* pattern as `providers/registry/stability-ai/imageModels.ts` and
* `providers/registry/segmind/imageModels.ts`. Co-located with the existing
* `gemini/index.ts` chat-provider entry — same provider id, different
* modality/consumer (chat registry vs image registry), mirroring the
* `kie/index.ts` + `kie/imageModels.ts` split.
*/
export const GEMINI_IMAGEN_PROVIDER = {
id: "gemini",
alias: "gemini",
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
authType: "apikey",
authHeader: "x-goog-api-key",
format: "google-imagen",
models: [
{ id: "imagen-4.0-generate-001", name: "Imagen 4" },
{ id: "imagen-4.0-ultra-generate-001", name: "Imagen 4 Ultra" },
{ id: "imagen-4.0-fast-generate-001", name: "Imagen 4 Fast" },
],
supportedSizes: ["1024x1024", "1792x1024", "1024x1792"],
};

View File

@@ -22,14 +22,14 @@ export const geminiProvider: RegistryEntry = {
},
models: [
{
id: "gemini-3.1-pro-preview",
name: "Gemini 3.1 Pro Preview",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
toolCalling: true,
supportsVision: true,
},
{
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview",
id: "gemini-3.1-pro-preview",
name: "Gemini 3.1 Pro Preview",
toolCalling: true,
supportsVision: true,
},
@@ -40,8 +40,8 @@ export const geminiProvider: RegistryEntry = {
supportsVision: true,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview",
toolCalling: true,
supportsVision: true,
},

View File

@@ -24,8 +24,8 @@ export const gemini_webProvider: RegistryEntry = {
supportsReasoning: false,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
toolCalling: false,
supportsReasoning: false,
},

View File

@@ -97,14 +97,29 @@ export const gheCopilotProvider: RegistryEntry = {
maxOutputTokens: 64000,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
contextLength: 1000000,
maxOutputTokens: 64000,
},
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", targetFormat: "openai-responses", maxOutputTokens: 128000 },
{ id: "gpt-5.6-terra", name: "GPT-5.6 Terra", targetFormat: "openai-responses", maxOutputTokens: 128000 },
{ id: "gpt-5.6-luna", name: "GPT-5.6 Luna", targetFormat: "openai-responses", maxOutputTokens: 128000 },
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
{
id: "gpt-5.6-terra",
name: "GPT-5.6 Terra",
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
{
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
{ id: "gpt-5.5", name: "GPT-5.5", ...GPT_5_5_CODEX_CAPABILITIES, maxOutputTokens: 128000 },
{
id: "gpt-5.4",

View File

@@ -117,8 +117,8 @@ export const githubProvider: RegistryEntry = {
maxOutputTokens: 64000,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
contextLength: 1000000,
maxOutputTokens: 64000,
},

View File

@@ -23,9 +23,6 @@ export const KIE_IMAGE_MODELS: KieImageModelEntry[] = [
{ id: "seedream/5.0-lite-image-to-image", name: "Seedream 5.0 Lite I2I", isMarket: true },
{ id: "z-image/4.0-text-to-image", name: "Z-Image v4.0", isMarket: true },
{ id: "z-image/4.5-text-to-image", name: "Z-Image v4.5", isMarket: true },
{ id: "google-imagen/imagen4-fast", name: "Imagen 4 Fast", isMarket: true },
{ id: "google-imagen/imagen4-ultra", name: "Imagen 4 Ultra", isMarket: true },
{ id: "google-imagen/imagen4", name: "Imagen 4", isMarket: true },
{ id: "google-imagen/nano-banana-2", name: "Nano Banana 2", isMarket: true },
{ id: "google-imagen/nano-banana", name: "Nano Banana", isMarket: true },
{ id: "google-imagen/nano-banana-pro", name: "Nano Banana Pro", isMarket: true },

View File

@@ -12,16 +12,15 @@ export const kieProvider: RegistryEntry = {
models: [
// Sweep 2026-06-19: + current flagships the kie proxy surfaces. gemini-3-pro was
// skipped (registry already carries the newer gemini-3-1-pro).
{ id: "claude-opus-4-8", name: "Claude 4.8 Opus" },
{ id: "claude-opus-4-7", name: "Claude 4.7 Opus" },
{ id: "claude-sonnet-4-6", name: "Claude 4.6 Sonnet" },
{ id: "claude-fable-5", name: "Claude 5 Fable" },
{ id: "claude-opus-5", name: "Claude 5 Opus" },
{ id: "claude-sonnet-5", name: "Claude 5 Sonnet" },
{ id: "claude-haiku-4-5", name: "Claude 4.5 Haiku" },
{ id: "gpt-5-5", name: "GPT 5.5" },
{ id: "gpt-5-4", name: "GPT 5.4" },
{ id: "gpt-5-2", name: "GPT 5.2" },
{ id: "gpt-5-6-sol", name: "GPT 5.6 Sol" },
{ id: "gpt-5-6-terra", name: "GPT 5.6 Terra" },
{ id: "gpt-5-6-luna", name: "GPT 5.6 Luna" },
{ id: "gemini-3-1-pro", name: "Gemini 3.1 Pro" },
{ id: "gemini-2-5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
{ id: "gemini-3-5-flash", name: "Gemini 3.5 Flash" },
{ id: "gemini-3-7-flash", name: "Gemini 3.7 Flash" },
{ id: "grok-4-6", name: "Grok 4.6" },
],
};

View File

@@ -11,9 +11,6 @@ export const KIE_IMAGE_MODELS = [
{ id: "seedream/5.0-lite-image-to-image", name: "Seedream 5.0 Lite I2I", isMarket: true },
{ id: "z-image/4.0-text-to-image", name: "Z-Image v4.0", isMarket: true },
{ id: "z-image/4.5-text-to-image", name: "Z-Image v4.5", isMarket: true },
{ id: "google-imagen/imagen4-fast", name: "Imagen 4 Fast", isMarket: true },
{ id: "google-imagen/imagen4-ultra", name: "Imagen 4 Ultra", isMarket: true },
{ id: "google-imagen/imagen4", name: "Imagen 4", isMarket: true },
{ id: "google-imagen/nano-banana-2", name: "Nano Banana 2", isMarket: true },
{ id: "google-imagen/nano-banana", name: "Nano Banana", isMarket: true },
{ id: "google-imagen/nano-banana-pro", name: "Nano Banana Pro", isMarket: true },

View File

@@ -25,19 +25,20 @@ export const kilocodeProvider: RegistryEntry = {
},
models: [
{ id: "openrouter/free", name: "Free Models Router" },
{ id: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus" },
{ id: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B" },
{ id: "openai/gpt-5.5", name: "GPT-5.5" },
{ id: "openai/gpt-5.4-mini", name: "GPT-5.4 Mini" },
{ id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7" },
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "openai/gpt-5.6-sol", name: "GPT-5.6 Sol" },
{ id: "openai/gpt-5.6-terra", name: "GPT-5.6 Terra" },
{ id: "openai/gpt-5.6-luna", name: "GPT-5.6 Luna" },
{ id: "anthropic/claude-opus-5", name: "Claude Opus 5" },
{ id: "anthropic/claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5" },
{ id: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" },
{ id: "google/gemini-3-flash-preview", name: "Gemini 3 Flash" },
{ id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite" },
{ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
{ id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
{ id: "moonshotai/kimi-k2.6", name: "Kimi K2.6" },
{ id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash" },
{ id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" },
{ id: "qwen/qwen3.8-max", name: "Qwen3.8 Max" },
{ id: "qwen/qwen3.7-plus", name: "Qwen3.7 Plus" },
{ id: "deepseek/deepseek-v4-pro-0813", name: "DeepSeek V4 Pro", supportsReasoning: true },
{ id: "deepseek/deepseek-v4-flash-0731", name: "DeepSeek V4 Flash", supportsReasoning: true },
{ id: "moonshotai/kimi-k3", name: "Kimi K3" },
],
passthroughModels: true,
};

View File

@@ -90,10 +90,10 @@ export const LMARENA_DIRECT_MODEL_ENTRIES: readonly LmarenaDirectModelEntry[] =
category: "Text",
},
{
catalogId: "gemini-3.5-flash-high",
arenaId: "019f406f-fc33-7b9d-9571-7b8443bc7ca0",
publicName: "gemini-3.5-flash-high",
displayName: "gemini-3.5-flash-high",
catalogId: "gemini-3.6-flash",
arenaId: "019f90b1-c0ac-71ce-b295-487f261bf0f4",
publicName: "gemini-3.6-flash",
displayName: "gemini-3.6-flash",
organization: "google",
vision: true,
category: "Text",

View File

@@ -25,55 +25,48 @@ export const opencode_zenProvider: RegistryEntry = {
supportsReasoning: true,
interleavedField: "reasoning_content",
},
{ id: "gpt-5-nano", name: "GPT 5 Nano", contextLength: 400000 },
{ id: "gpt-5", name: "GPT 5" },
{ id: "gpt-5-codex", name: "GPT 5 Codex" },
{ id: "gpt-5.1", name: "GPT 5.1" },
{ id: "gpt-5.1-codex", name: "GPT 5.1 Codex" },
{ id: "gpt-5.1-codex-max", name: "GPT 5.1 Codex Max" },
{ id: "gpt-5.1-codex-mini", name: "GPT 5.1 Codex Mini" },
{ id: "gpt-5.2", name: "GPT 5.2" },
{ id: "gpt-5.2-codex", name: "GPT 5.2 Codex" },
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
{ id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" },
{ id: "gpt-5.6-sol", name: "GPT 5.6 Sol" },
{ id: "gpt-5.6-terra", name: "GPT 5.6 Terra" },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna" },
{ id: "gpt-5.4", name: "GPT 5.4" },
{ id: "gpt-5.4-mini", name: "GPT 5.4 Mini" },
{ id: "gpt-5.4-nano", name: "GPT 5.4 Nano" },
{ id: "gpt-5.4-pro", name: "GPT 5.4 Pro" },
{ id: "gpt-5.5", name: "GPT 5.5" },
{ id: "gpt-5.5-pro", name: "GPT 5.5 Pro" },
{ id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" },
{ id: "gpt-5.1", name: "GPT 5.1" },
// ── Claude ─────────────────────────────────────────────────
{ id: "claude-fable-5", name: "Claude Fable 5" },
{ id: "claude-opus-5", name: "Claude Opus 5" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-opus-4-1", name: "Claude Opus 4.1" },
{ id: "claude-opus-4-5", name: "Claude Opus 4.5" },
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
// ── Gemini ─────────────────────────────────────────────────
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
{ id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" },
{ id: "gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" },
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
// ── Grok ───────────────────────────────────────────────────
{ id: "grok-build-0.1", name: "Grok Build 0.1" },
{ id: "grok-4.6", name: "Grok 4.6" },
// ── Muse ───────────────────────────────────────────────────
{ id: "muse-spark-1.2", name: "Muse Spark 1.2" },
// ── DeepSeek ────────────────────────────────────────────────
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
// ── GLM / Z.AI ─────────────────────────────────────────────
{ id: "glm-5", name: "GLM-5" },
{ id: "glm-5.1", name: "GLM-5.1" },
{ id: "glm-5.2", name: "GLM-5.2" },
// ── MiniMax ────────────────────────────────────────────────
// #3110: MiniMax M3 — frontier coding model with 1M context
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "minimax-m2.5", name: "MiniMax M2.5" },
{ id: "minimax-m2.7", name: "MiniMax M2.7" },
// ── Kimi / Moonshot ────────────────────────────────────────
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k3", name: "Kimi K3" },
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
// ── Qwen ───────────────────────────────────────────────────
// Issue #2292: Qwen models return Claude-format SSE bodies even
@@ -86,12 +79,14 @@ export const opencode_zenProvider: RegistryEntry = {
// ── Free Tier ──────────────────────────────────────────────
// #6998 (2026-07-14): upstream free tier rotated — minimax-m2.5-free,
// nemotron-3-super-free and qwen3.6-plus-free were delisted (401). Replaced
// by the 4 entries below with upstream-verified limits.
// nemotron-3-super-free and qwen3.6-plus-free were delisted (401).
// 2026-08-17 sync: north-mini-code-free delisted; nemotron-3.5-lightning-free
// and laguna-s-2.1-free added.
{ id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true },
{ id: "mimo-v2.5-free", name: "MiMo V2.5 Free", contextLength: 200000 },
{ id: "hy3-free", name: "HY3 Free", contextLength: 200000 },
{ id: "nemotron-3-ultra-free", name: "Nemotron 3 Ultra Free", contextLength: 1000000 },
{ id: "north-mini-code-free", name: "North Mini Code Free", contextLength: 200000 },
{ id: "nemotron-3.5-lightning-free", name: "Nemotron 3.5 Lightning Free" },
{ id: "laguna-s-2.1-free", name: "Laguna S 2.1 Free" },
],
};

View File

@@ -35,8 +35,8 @@ export const orcarouterProvider: RegistryEntry = {
maxOutputTokens: 128000,
},
{
id: "google/gemini-3.5-flash",
name: "Gemini 3.5 Flash",
id: "google/gemini-3.6-flash",
name: "Gemini 3.6 Flash",
toolCalling: true,
supportsReasoning: true,
supportsVision: true,

View File

@@ -13,12 +13,12 @@ export const perplexity_webProvider: RegistryEntry = {
{ id: "pplx-sonar", name: "Sonar 2 (via Perplexity)", toolCalling: false },
{ id: "pplx-gpt-5.6-terra", name: "GPT-5.6 Terra (via Perplexity)", toolCalling: false },
{ id: "pplx-gpt-5.6-sol", name: "GPT-5.6 Sol (via Perplexity)", toolCalling: false },
{ id: "pplx-gemini", name: "Gemini 3.1 Pro (via Perplexity)", toolCalling: false },
{ id: "pplx-gemini", name: "Gemini 3.7 Flash (via Perplexity)", toolCalling: false },
{ id: "pplx-sonnet", name: "Claude Sonnet 5.0 (via Perplexity)", toolCalling: false },
{ id: "pplx-opus", name: "Claude Opus 5.0 (via Perplexity)", toolCalling: false },
{ id: "pplx-glm", name: "GLM-5.2 (via Perplexity)", toolCalling: false },
{ id: "pplx-kimi", name: "Kimi K2.6 (via Perplexity)", toolCalling: false },
{ id: "pplx-grok-4.5", name: "Grok 4.5 (via Perplexity)", toolCalling: false },
{ id: "pplx-kimi", name: "Kimi K3 (via Perplexity)", toolCalling: false },
{ id: "pplx-grok-4.6", name: "Grok 4.6 (via Perplexity)", toolCalling: false },
{ id: "pplx-nemotron", name: "Nemotron 3 Ultra (via Perplexity)", toolCalling: false },
],
};

View File

@@ -19,15 +19,43 @@ export const raycastProvider: RegistryEntry = {
authHeader: "bearer",
defaultContextLength: 128000,
models: [
{ id: "openai-gpt-5-mini", name: "GPT-5 Mini" },
{ id: "openai-gpt-4o-mini", name: "GPT-4o Mini" },
{ id: "anthropic-claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "google-gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "raycast-ray1", name: "Ray1" },
{ id: "raycast-ray1-mini", name: "Ray1 Mini" },
{ id: "perplexity-sonar", name: "Sonar" },
//GPT
{ id: "openai-gpt-5.6-sol", name: "GPT-5.6 Sol" },
{ id: "openai-gpt-5.6-terra", name: "GPT-5.6 Terra" },
{ id: "openai-gpt-5.6-luna", name: "GPT-5.6 Luna" },
//Claude
{ id: "anthropic-claude-opus-5", name: "Claude Opus 5" },
{ id: "anthropic-claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "anthropic-claude-4-5-haiku-reasoning", name: "Claude 4.5 Haiku Reasoning" },
{ id: "anthropic-claude-4-5-haiku", name: "Claude 4.5 Haiku" },
//Gemini
{ id: "google-gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "google-gemini-3.7-flash", name: "Gemini 3.7 Flash" },
{ id: "google-gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite" },
//Perplexity
{ id: "perplexity-sonar-reasoning-pro", name: "Sonar Reasoning Pro" },
{ id: "perplexity-sonar-pro", name: "Sonar Pro" },
{ id: "perplexity-sonar", name: "Sonar" },
//Mistral
{ id: "mistral-mistral-large-latest", name: "Mistral Large" },
{ id: "mistral-mistral-medium-latest", name: "Mistral Medium" },
{ id: "mistral-mistral-small-latest", name: "Mistral Small" },
{ id: "mistral-codestral-latest", name: "Codestral" },
{ id: "mistral-open-mistral-nemo", name: "Mistral Nemo" },
{ id: "xai-grok-3-mini", name: "Grok 3 Mini" },
//Grok
{ id: "xai-grok-4.6", name: "Grok 4.6" },
//Opensource
{ id: "gateway-alibaba/qwen3.8-max", name: "Qwen 3.8 Max" },
{ id: "gateway-moonshotai/kimi-k3", name: "Kimi K3" },
{ id: "baseten-deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" },
{ id: "gateway-deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "baseten-zai-org/GLM-5.2", name: "GLM 5.2" },
{ id: "gateway-thinkingmachines/inkling-1.0", name: "Inkling 1.0" },
{ id: "gateway-google/gemma-4-31b-it", name: "Gemma 4 31B" },
{ id: "groq-openai/gpt-oss-120b", name: "GPT-OSS 120B" },
{ id: "groq-openai/gpt-oss-20b", name: "GPT-OSS 20B" },
{ id: "groq-qwen/qwen3-32b", name: "Qwen 3 32B" },
{ id: "groq-llama-3.3-70b-versatile", name: "LLaMA 3.3 70B" },
{ id: "groq-llama-3.1-8b-instant", name: "LLaMA 3.1 8B" },
],
};

View File

@@ -18,22 +18,28 @@ export const tinycmsProvider: RegistryEntry = {
authType: "apikey",
authHeader: "uuid",
models: [
{ id: "gpt-5-free", name: "GPT 5 Free" },
{ id: "gpt-5.3-free", name: "GPT 5.3 Free (Multimodal/Vision)" },
{ id: "gpt-5.3-thinking-free", name: "GPT 5.3 Thinking Free", supportsReasoning: true },
{ id: "claude-fable-5", name: "Claude Fable 5" },
{ id: "claude-opus-5", name: "Claude Opus 5" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "gpt-5.6-sol", name: "GPT 5.6 Sol" },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna" },
{ id: "gpt-5.5", name: "GPT 5.5" },
{ id: "gpt-5.4-mini", name: "GPT 5.4 Mini" },
{ id: "gpt-5.4-nano", name: "GPT 5.4 Nano" },
{ id: "gpt-5-nano", name: "GPT 5 Nano" },
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "grok-4.20-fast", name: "Grok 4.20 Fast" },
{ id: "grok-4.20", name: "Grok 4.20" },
{ id: "grok-imagine", name: "Grok Imagine (Image Gen)" },
{ id: "gpt-5.3-thinking-free", name: "GPT 5.3 Thinking Free", supportsReasoning: true },
{ id: "gpt-5.3-free", name: "GPT 5.3 Free (Multimodal/Vision)" },
{ id: "gpt-oss-120b", name: "GPT-OSS 120B" },
{ id: "gemini-3.6-flash", name: "Gemini 3.6 Flash" },
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite" },
{ id: "grok-4.5", name: "Grok 4.5" },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "gpt-image-2", name: "GPT Image 2 (Image Gen)" },
{ id: "kimi-k3", name: "Kimi K3" },
{ id: "glm-5.2", name: "GLM 5.2" },
{ id: "qwen3.6-plus", name: "Qwen 3.6 Plus" },
{ id: "mimo-v2.5-pro", name: "Mimo V2.5 Pro" },
{ id: "mimo-v2.5", name: "Mimo V2.5" },
],
};

View File

@@ -1,4 +1,5 @@
import { getExplicitModelOutputCap } from "@/lib/modelCapabilities";
import { isDiscoverableAntigravityModelId } from "../config/antigravityModelAliases";
/**
* Fallback ceiling on `generationConfig.maxOutputTokens` for Antigravity
@@ -13,7 +14,7 @@ import { getExplicitModelOutputCap } from "@/lib/modelCapabilities";
* Both of those models are catalogue-known today, so neither one reaches this
* constant anymore: they get their own declared limit via
* `resolveAntigravityOutputCap` (65536 and 65535, respectively). The higher
* limit holds against the live upstream. A gemini-3.6-flash-high request came
* limit holds against the live upstream. A gemini-3.7-flash-high request came
* back with completion_tokens 16754 and finish_reason "stop", which exceeds
* 16384 on its own and so cannot be an artifact of thinking-token accounting.
*
@@ -38,6 +39,10 @@ export const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384;
export function resolveAntigravityOutputCap(modelId: string | null | undefined): number {
const id = typeof modelId === "string" ? modelId.trim() : "";
if (!id) return MAX_ANTIGRAVITY_OUTPUT_TOKENS;
// MODEL_SPECS is provider-neutral: other providers may continue serving old
// Gemini 3.5/3.6 ids after Antigravity retires them. Do not let those shared
// specs make a retired Antigravity id look active on this provider path.
if (!isDiscoverableAntigravityModelId(id)) return MAX_ANTIGRAVITY_OUTPUT_TOKENS;
try {
const declared = getExplicitModelOutputCap({ provider: "antigravity", model: id });
return typeof declared === "number" && Number.isFinite(declared) && declared > 0

View File

@@ -69,24 +69,26 @@ export const MODEL_MAP: Record<string, [string, string]> = {
"pplx-sonar": ["copilot", "turbo"],
"pplx-gpt-5.6-terra": ["copilot", "gpt56_terra"],
"pplx-gpt-5.6-sol": ["copilot", "gpt56_sol"],
"pplx-gemini": ["copilot", "gemini31pro_high"],
"pplx-gemini": ["copilot", "gemini37flash"],
"pplx-sonnet": ["copilot", "claude50sonnet"],
// Perplexity's catalog moved Opus to 5.0; claude48opus is still accepted but
// answers from the older model.
"pplx-opus": ["copilot", "claude50opus"],
"pplx-glm": ["copilot", "glm_5_2"],
"pplx-kimi": ["copilot", "kimik26instant"],
"pplx-grok-4.5": ["copilot", "grok45low"],
// The current Kimi K3 catalog entry only exposes its reasoning model.
"pplx-kimi": ["copilot", "kimik3thinking"],
"pplx-grok-4.6": ["copilot", "grok46low"],
"pplx-nemotron": ["copilot", "nv_nemotron_3_ultra"],
};
export const THINKING_MAP: Record<string, string> = {
"pplx-gpt-5.6-terra": "gpt56_terra_thinking",
"pplx-gpt-5.6-sol": "gpt56_sol_thinking",
"pplx-gemini": "gemini37flashthinking",
"pplx-sonnet": "claude50sonnetthinking",
"pplx-opus": "claude50opusthinking",
"pplx-kimi": "kimik26thinking",
"pplx-grok-4.5": "grok45medium",
"pplx-kimi": "kimik3thinking",
"pplx-grok-4.6": "grok46medium",
};
export const CITATION_RE = /\[\d+\]/g;

View File

@@ -89,6 +89,7 @@ import { checkResourcePressureGuard } from "../utils/resourcePressure.ts";
import { normalizeHeaders } from "../utils/headers.ts";
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
import { resolveOmniGlyphTransport } from "../services/compression/imageTransportPolicy.ts";
import { stripStore, usesClaudeBridge } from "./chatCore/agentRouterProtocol.ts";
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
@@ -1611,16 +1612,13 @@ export async function handleChatCore({
// models, which is intentionally NOT `false` so the gate still preserves images.
supportsVision: getResolvedModelCapabilities({ provider, model: effectiveModel })
.supportsVision,
// Rotas diretas oficiais ('anthropic' API key e 'claude' OAuth) vs agregadores:
// o engine omniglyph exige 'direct' — agregadores redimensionam imagens
// (medido 2026-07-06). OAuth 'claude' é rota direta oficial (#7863).
providerTransport:
provider === "anthropic" ||
provider === "claude" ||
provider === "openai" ||
provider === "xai"
? ("direct" as const)
: ("aggregator" as const),
// OmniGlyph uses a measured provider/image-fidelity allowlist. Direct HTTP
// alone is not proof that a route preserves PNG bytes and dimensions.
...resolveOmniGlyphTransport(provider),
// Sem o provider, a contabilidade do OmniGlyph cai para `unknown` e
// recusa deduzir a semântica de cache (Anthropic usa buckets disjuntos,
// OpenAI reporta cached como subconjunto do input).
provider,
sourceFormat,
targetFormat,
compressionStage: "pre-translation" as const,

View File

@@ -41,7 +41,6 @@ import { handleHyperbolicImageGeneration } from "./imageGeneration/providers/hyp
import { handleHuggingFaceImageGeneration } from "./imageGeneration/providers/huggingface.ts";
import { handleComfyUIImageGeneration } from "./imageGeneration/providers/comfyUI.ts";
import { handleImagen3ImageGeneration } from "./imageGeneration/providers/imagen3.ts";
import { handleGoogleImagenGeneration } from "./imageGeneration/providers/googleImagen.ts";
import { handleIdeogramImageGeneration } from "./imageGeneration/providers/ideogram.ts";
import { handleHaiperImageGeneration } from "./imageGeneration/providers/haiper.ts";
import { handleLeonardoImageGeneration } from "./imageGeneration/providers/leonardo.ts";
@@ -383,17 +382,6 @@ export async function handleImageGeneration({
});
}
if (providerConfig.format === "google-imagen") {
return handleGoogleImagenGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
if (providerConfig.format === "hyperbolic") {
return handleHyperbolicImageGeneration({
model,

View File

@@ -1,147 +0,0 @@
// Google AI Studio (Gemini API) Imagen image generation.
//
// Unlike the antigravity "gemini-image" format (which wraps generateContent in a
// Cloud Code envelope), the Imagen family on generativelanguage.googleapis.com uses
// the dedicated ":predict" endpoint with an instances/parameters body and returns
// base64 image bytes under `predictions[].bytesBase64Encoded`.
//
// Docs: https://ai.google.dev/gemini-api/docs/imagen (Imagen requires a billing-
// enabled Google project; free-tier keys get 403 / quota 0.)
import { saveCallLog } from "@/lib/usageDb";
import { mapImageSize } from "../../../translator/image/sizeMapper.ts";
import { sanitizeErrorMessage } from "../../../utils/error.ts";
// Only the Imagen family routes through :predict. Other gemini image models
// (gemini-*-flash-image / nano-banana) use generateContent and belong on the chat
// route, so they must not be dispatched here.
export function isImagenModel(model) {
return /^imagen-/i.test(String(model || ""));
}
/**
* Build the Imagen :predict request body from an OpenAI-style image request.
* Pure — no I/O — so it can be unit-tested without live credentials.
*/
export function buildImagenPredictBody(body) {
const prompt = typeof body?.prompt === "string" ? body.prompt : String(body?.prompt ?? "");
const n = Number(body?.n);
const sampleCount = Number.isFinite(n) && n > 0 ? Math.min(Math.floor(n), 4) : 1;
return {
instances: [{ prompt }],
parameters: {
sampleCount,
aspectRatio: mapImageSize(body?.aspect_ratio || body?.size),
},
};
}
/**
* Normalize an Imagen :predict response into the OpenAI image-generation shape
* ({ created, data: [{ b64_json, revised_prompt }] }). Pure — unit-testable.
*/
export function parseImagenPredictResponse(data, prompt) {
const predictions = Array.isArray(data?.predictions) ? data.predictions : [];
const images = [];
for (const p of predictions) {
const b64 = p?.bytesBase64Encoded ?? p?.b64_json ?? p?.image ?? null;
if (typeof b64 === "string" && b64.length > 0) {
images.push({ b64_json: b64, revised_prompt: prompt });
}
}
return { created: Math.floor(Date.now() / 1000), data: images };
}
export async function handleGoogleImagenGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || credentials?.accessToken || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (!isImagenModel(model)) {
return {
success: false,
status: 400,
error: `Model ${model} is not an Imagen model. Gemini flash-image models route through /v1/chat/completions, not /v1/images/generations.`,
};
}
const upstreamBody = buildImagenPredictBody(body);
// baseUrl is https://generativelanguage.googleapis.com/v1beta/models
const url = `${providerConfig.baseUrl.replace(/\/$/, "")}/${model}:predict`;
if (log) {
log.info(
"IMAGE",
`${provider}/${model} (google-imagen) | prompt: "${prompt.slice(0, 60)}..." | aspectRatio: ${upstreamBody.parameters.aspectRatio}`
);
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
// Key travels in the header, never the URL, so it stays out of logs.
"x-goog-api-key": token,
},
body: JSON.stringify(upstreamBody),
});
if (!response.ok) {
const errorText = await response.text();
const safeError = sanitizeErrorMessage(errorText);
if (log) log.error("IMAGE", `${provider} error ${response.status}: ${safeError.slice(0, 200)}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: response.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: safeError.slice(0, 500),
}).catch(() => {});
return { success: false, status: response.status, error: safeError };
}
const data = await response.json();
const normalized = parseImagenPredictResponse(data, prompt);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { images_count: normalized.data.length },
}).catch(() => {});
return { success: true, data: normalized };
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
if (log) log.error("IMAGE", `${provider} fetch error: ${errMsg}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errMsg,
}).catch(() => {});
return {
success: false,
status: 502,
error: `Image provider error: ${sanitizeErrorMessage(errMsg)}`,
};
}
}

View File

@@ -22,24 +22,24 @@ describe("Antigravity account quota-family cooldown", () => {
});
it("maps Gemini variants to Gemini family and Claude/Cloud variants to Claude family", () => {
expect(getAntigravityQuotaFamily("gemini-3.5-flash-medium")).toBe("gemini");
expect(getAntigravityQuotaFamily("google/gemini-3.5-flash-low")).toBe("gemini");
expect(getAntigravityQuotaFamily("agy/gemini-3.5-flash-medium")).toBe("gemini");
expect(getAntigravityQuotaFamily("gemini-3.7-flash-medium")).toBe("gemini");
expect(getAntigravityQuotaFamily("google/gemini-3.7-flash-low")).toBe("gemini");
expect(getAntigravityQuotaFamily("agy/gemini-3.7-flash-medium")).toBe("gemini");
expect(getAntigravityQuotaFamily("claude-sonnet-4")).toBe("claude");
expect(getAntigravityQuotaFamily("cloud/claude-opus-4")).toBe("claude");
expect(getAntigravityQuotaFamily("some-new-model")).toBe("other");
});
it("uses family-scoped lock key for Antigravity but preserves exact-model scope elsewhere", () => {
expect(getQuotaScopedModelForProvider("antigravity", "gemini-3.5-flash-medium")).toBe(
expect(getQuotaScopedModelForProvider("antigravity", "gemini-3.7-flash-medium")).toBe(
"family:gemini"
);
expect(getQuotaScopedModelForProvider("agy", "gemini-3.5-flash-medium")).toBe("family:gemini");
expect(getQuotaScopedModelForProvider(provider, "gemini-3.5-flash-low")).toBe("family:gemini");
expect(getQuotaScopedModelForProvider("agy", "gemini-3.7-flash-medium")).toBe("family:gemini");
expect(getQuotaScopedModelForProvider(provider, "gemini-3.7-flash-low")).toBe("family:gemini");
expect(getQuotaScopedModelForProvider(provider, "claude-sonnet-4")).toBe("family:claude");
expect(getQuotaScopedModelForProvider(provider, "unknown-model")).toBe("unknown-model");
expect(getQuotaScopedModelForProvider("openai", "gemini-3.5-flash-medium")).toBe(
"gemini-3.5-flash-medium"
expect(getQuotaScopedModelForProvider("openai", "gemini-3.7-flash-medium")).toBe(
"gemini-3.7-flash-medium"
);
});
@@ -47,7 +47,7 @@ describe("Antigravity account quota-family cooldown", () => {
recordModelLockoutFailure(
provider,
"account-a",
"gemini-3.5-flash-medium",
"gemini-3.7-flash-medium",
"rate_limited",
429,
60_000,
@@ -55,10 +55,10 @@ describe("Antigravity account quota-family cooldown", () => {
{ maxCooldownMs: 300_000 }
);
expect(isModelLocked(provider, "account-a", "gemini-3.5-flash-medium")).toBe(true);
expect(isModelLocked(provider, "account-a", "gemini-3.5-flash-low")).toBe(true);
expect(isModelLocked(provider, "account-a", "gemini-3.7-flash-medium")).toBe(true);
expect(isModelLocked(provider, "account-a", "gemini-3.7-flash-low")).toBe(true);
expect(isModelLocked(provider, "account-a", "claude-sonnet-4")).toBe(false);
expect(isModelLocked(provider, "account-b", "gemini-3.5-flash-low")).toBe(false);
expect(isModelLocked(provider, "account-b", "gemini-3.7-flash-low")).toBe(false);
});
it("keeps Claude/Cloud family distinct from Gemini", () => {
@@ -74,7 +74,7 @@ describe("Antigravity account quota-family cooldown", () => {
);
expect(isModelLocked(provider, "account-a", "cloud/claude-opus-4")).toBe(true);
expect(isModelLocked(provider, "account-a", "gemini-3.5-flash-low")).toBe(false);
expect(isModelLocked(provider, "account-a", "gemini-3.7-flash-low")).toBe(false);
});
it("can isolate a confirmed Antigravity quota exhaustion to one exact model", () => {
@@ -88,7 +88,7 @@ describe("Antigravity account quota-family cooldown", () => {
expect(isModelLocked(provider, "account-a", "claude-opus-4-6-thinking")).toBe(true);
expect(isModelLocked(provider, "account-a", "claude-sonnet-4-6-thinking")).toBe(false);
expect(isModelLocked(provider, "account-a", "gemini-3.5-flash-medium")).toBe(false);
expect(isModelLocked(provider, "account-a", "gemini-3.7-flash-medium")).toBe(false);
expect(clearModelLock(provider, "account-a", "claude-opus-4-6-thinking")).toBe(true);
expect(isModelLocked(provider, "account-a", "claude-opus-4-6-thinking")).toBe(false);
@@ -113,7 +113,7 @@ describe("Antigravity account quota-family cooldown", () => {
const upstream = recordModelLockoutFailure(
provider,
"account-a",
"gemini-3.5-flash-medium",
"gemini-3.7-flash-medium",
"rate_limited",
429,
1_000,
@@ -122,13 +122,13 @@ describe("Antigravity account quota-family cooldown", () => {
);
expect(upstream.cooldownMs).toBe(123_000);
expect(
getModelLockoutInfo(provider, "account-a", "gemini-3.5-flash-low")?.remainingMs
getModelLockoutInfo(provider, "account-a", "gemini-3.7-flash-low")?.remainingMs
).toBeGreaterThan(100_000);
const inferred = recordModelLockoutFailure(
provider,
"account-b",
"gemini-3.5-flash-medium",
"gemini-3.7-flash-medium",
"rate_limited",
429,
1_000,

View File

@@ -8,7 +8,7 @@
*
* ## Fail-open paths
* 1. Optional-deps gate: if any of `@atjsh/llmlingua-2`, `@huggingface/transformers`,
* `@tensorflow/tfjs`, `js-tiktoken` does not resolve, return `text` immediately —
* `js-tiktoken` does not resolve, return `text` immediately —
* NO worker spawn. This is the default in CI / most installs (deps are OPTIONAL).
* 2. Per-call timeout: first call for a model gets `FIRST_CALL_TIMEOUT_MS` (one-time
* model load); warm calls get `LLMLINGUA_WORKER_TIMEOUT_MS`. On timeout → original
@@ -16,7 +16,7 @@
* 3. Worker error/exit → resolve all pending with their original text + respawn next.
*
* ## Serialization
* ONNX/tfjs are not reentrant — calls are queued FIFO and only one message is
* ONNX inference is not reentrant — calls are queued FIFO and only one message is
* in-flight at a time (the next is posted after the previous reply or its timeout).
*
* ## Idle eviction
@@ -45,7 +45,7 @@ const FIRST_CALL_TIMEOUT_MS = 60000;
/**
* Gate probe: `@atjsh/llmlingua-2` is the entry package that declares the others
* (`@huggingface/transformers`, `@tensorflow/tfjs`, `js-tiktoken`) as peers. We probe
* (`@huggingface/transformers`, `js-tiktoken`) as peers. We probe
* ONLY it (by manifest existence) because the peers are ESM-only and `require.resolve`
* throws for them even when installed; the worker still fail-opens if a peer is
* genuinely missing at `import()` time.

View File

@@ -9,6 +9,7 @@
* - modelo fora da allowlist medida → skip:model_not_approved
* - providerTransport !== 'direct' → skip:transport_not_direct
* (agregadores redimensionam imagens e destroem a legibilidade — medido)
* - imageTransportFidelity !== 'byte-preserving' → skip:transport_fidelity_unknown/resizes
* - wire não é Claude/OpenAI suportado → skip:target_format_not_supported
* - gate de rentabilidade interno do omniglyph decide o resto (patches 28px
* exatos; texto esparso/pequeno passa direto) → skip:not_profitable
@@ -20,14 +21,65 @@ import type { CompressionEngine, CompressionEngineApplyOptions } from "./types.t
import type { CompressionResult } from "../types.ts";
import { createCompressionStats } from "../stats.ts";
import {
isOmniGlyphSupportedGptModel,
isOmniGlyphSupportedModel,
buildOmniGlyphAccounting,
type OmniGlyphAccounting,
} from "../omniglyphTelemetry.ts";
import {
isOmniGlyphSupportedModelForScope,
mergeCompressionProfileOptions,
resolveCompressionProfile,
transformAnthropicMessages,
transformOpenAIChatCompletions,
transformOpenAIResponses,
type CompressionProfile,
type OmniGlyphSafetyScope,
} from "omniglyph";
import { isModelImageable } from "omniglyph/applicability";
/**
* Teto de modelos do OmniRoute — sempre o escopo mais restrito do pacote.
*
* `isOmniGlyphSupportedModel()` resolve o escopo lendo `OMNIGLYPH_PROFILE` do
* processo, e a lista base sai de `OMNIGLYPH_MODELS`. Duas variáveis do HOST
* decidiriam, em silêncio, o gate de todo request do OmniRoute: `passthrough`
* desligaria a engine inteira e `OMNIGLYPH_MODELS` ADMITIRIA modelos sem
* recibo medido — enquanto a UI continua prometendo "Claude Fable 5 na rota
* direta medida". Fixar o escopo mais restrito faz o gate só poder ESTREITAR
* pela env, nunca alargar, e mantém a decisão na configuração do OmniRoute.
*/
const MEASURED_MODEL_SCOPE: OmniGlyphSafetyScope = "coding-safe";
/**
* Perfil padrão do OmniRoute.
*
* `aggressive` é a política que os recibos publicados mediram. `coding-safe` e
* `balanced` fixam `minCompressChars` no máximo e só colapsam histórico antigo:
* medido nesta base, uma sessão sem histórico acumulado fica em
* `below_min_chars` e a engine não faz nada — o operador veria "ligado, 0% de
* ganho". Ficam disponíveis como escolha explícita, não como default.
*/
const DEFAULT_PROFILE: OmniGlyphSafetyScope = "aggressive";
/** Perfil do passo (mais específico) > perfil global > default do OmniRoute. */
function resolveProfileName(options?: CompressionEngineApplyOptions): string {
const step = options?.stepConfig?.profile;
if (typeof step === "string" && step.trim()) return step;
const global = (options?.config as { omniglyph?: { profile?: unknown } } | undefined)?.omniglyph
?.profile;
if (typeof global === "string" && global.trim()) return global;
return DEFAULT_PROFILE;
}
/**
* O modelo precisa passar no teto medido E no escopo em vigor. Os dois wires
* (Anthropic e GPT) compartilham a mesma allowlist no pacote desde 1.4.0, então
* uma única checagem cobre os dois.
*/
function isModelWithinScope(model: string, scope: OmniGlyphSafetyScope): boolean {
if (!isOmniGlyphSupportedModelForScope(model, MEASURED_MODEL_SCOPE)) return false;
return isOmniGlyphSupportedModelForScope(model, scope);
}
function skip(body: Record<string, unknown>, reason: string): CompressionResult {
try {
return {
@@ -80,9 +132,36 @@ async function applyOmniglyph(
const model = options?.model ?? (body as { model?: string }).model ?? "";
if (options?.supportsVision !== true) return skip(body, "no_vision");
if (options?.providerTransport !== "direct") return skip(body, "transport_not_direct");
// Keep the old direct-call contract usable for standalone callers, but let
// production callers override it explicitly. The chat pipeline supplies
// `unknown` for every provider without a byte-preservation receipt.
if (
options?.imageTransportFidelity !== undefined &&
options.imageTransportFidelity !== "byte-preserving"
) {
return skip(
body,
options.imageTransportFidelity === "resizes"
? "transport_resizes_images"
: "transport_fidelity_unknown"
);
}
const stage = options?.compressionStage ?? "pre-translation";
const wireFormat = resolveWireFormat(body, options);
// A source/target format mismatch means the body is still on the wrong wire,
// even when the source itself is native Claude. Defer the engine until the
// translated provider body so Claude→OpenAI cannot be imaged once before
// translation and then considered again on the target wire.
const sourceWireFormat = options?.sourceFormat ?? wireFormat;
if (
stage === "pre-translation" &&
options?.targetFormat &&
sourceWireFormat &&
options.targetFormat !== sourceWireFormat
) {
return skip(body, "requires_post_translation");
}
// The pre-translation lane is retained for the existing native Claude
// passthrough. OpenAI requests must wait until translation has produced the
// exact provider wire, otherwise Responses input[] would be flattened by the
@@ -104,62 +183,98 @@ async function applyOmniglyph(
) {
return skip(body, "source_format_not_openai_responses");
}
const supportedModel =
wireFormat === "claude"
? isOmniGlyphSupportedModel(model)
: isOmniGlyphSupportedGptModel(model);
if (!supportedModel) return skip(body, "model_not_approved");
let profile: CompressionProfile;
try {
profile = resolveCompressionProfile(resolveProfileName(options));
} catch {
// `resolveCompressionProfile` lança em nome desconhecido. Um perfil que o
// pacote não entende não pode virar "roda com a política padrão".
return skip(body, "invalid_profile");
}
if (profile.name === "passthrough") return skip(body, "profile_passthrough");
if (!isModelWithinScope(model, profile.name)) {
return skip(body, "model_not_approved");
}
const preserveSystemPrompt =
(typeof options?.stepConfig?.preserveSystemPrompt === "boolean"
? options.stepConfig.preserveSystemPrompt
: options?.config?.preserveSystemPrompt) === true;
// `compressSystem` só existe no transform Anthropic. Os wires OpenAI honram
// apenas compressTools/gptHistory/minCompressChars/reflow e sempre trocam a
// instrução por um ponteiro para a imagem. Imagear o system quando o OmniRoute
// decidiu preservá-lo queimaria o prefixo quente que a política cache-aware
// está protegendo — e nada no corpo devolvido denunciaria isso. Sem como
// honrar a política nesse wire, a engine pula.
if (preserveSystemPrompt && wireFormat !== "claude") {
return skip(body, "system_preservation_unsupported_on_wire");
}
// OmniGlyph 1.3.x deliberately keeps unverified families (currently Grok)
// text-only until the operator acknowledges them via its own env gate.
if (!isModelImageable(model)) return skip(body, "model_not_imageable");
const started = Date.now();
let outBody: Record<string, unknown>;
let accounting: OmniGlyphAccounting | undefined;
try {
const encoded = new TextEncoder().encode(JSON.stringify(body));
// Branch explicitly so TS narrows each transformer's return type:
// the Anthropic wrapper reports `applied`, the OpenAI ones `info.compressed`.
let applied: boolean;
let transformed: { body: Uint8Array; info: { compressed: boolean; reason?: string } };
if (wireFormat === "claude") {
const result = await transformAnthropicMessages({ body: encoded, model });
transformed = result;
applied = result.applied;
} else {
const result =
wireFormat === "openai"
? await transformOpenAIChatCompletions(encoded)
: await transformOpenAIResponses(encoded);
transformed = result;
applied = result.info.compressed;
}
if (!applied) return skip(body, transformed.info?.reason ?? "not_profitable");
outBody = JSON.parse(new TextDecoder().decode(transformed.body)) as Record<string, unknown>;
// The upstream OpenAI transformer resolves its billing/render profile from
// body.model. Keep the provider body byte-compatible on output, but use the
// already-resolved engine model for that internal gate when a translator
// omitted the model or left an alias in place.
const transformBody =
wireFormat !== "claude" && model && body.model !== model ? { ...body, model } : body;
const encoded = new TextEncoder().encode(JSON.stringify(transformBody));
const overrides = preserveSystemPrompt ? { compressSystem: false } : {};
// Só `transformAnthropicMessages` resolve o perfil por conta própria; os
// transformadores OpenAI recebem TransformOptions cru e ignorariam o campo.
const openAIOptions = mergeCompressionProfileOptions(profile, overrides);
const result =
wireFormat === "claude"
? await transformAnthropicMessages({
body: encoded,
model,
options: { ...overrides, profile: profile.name },
})
: wireFormat === "openai"
? await transformOpenAIChatCompletions(encoded, openAIOptions)
: await transformOpenAIResponses(encoded, openAIOptions);
const applied = "applied" in result ? result.applied : result.info.compressed;
if (!applied) return skip(body, result.info?.reason ?? "not_profitable");
outBody = JSON.parse(new TextDecoder().decode(result.body)) as Record<string, unknown>;
if (transformBody !== body && body.model !== undefined) outBody.model = body.model;
accounting = buildOmniGlyphAccounting({
provider: options?.provider,
model,
originalBytes: encoded.byteLength,
transformedBytes: result.body.byteLength,
info: result.info,
durationMs: Date.now() - started,
});
} catch {
// Fail-open: qualquer erro no encode/transform/decode (ex.: corpo não serializável,
// render PNG estourando, JSON decodificado malformado) vira skip, nunca propaga.
return skip(body, "transform_error");
}
return {
body: outBody,
compressed: true,
stats: createCompressionStats(
body,
outBody,
"stacked",
["omniglyph:context-as-image"],
undefined,
Date.now() - started
),
};
const stats = createCompressionStats(
body,
outBody,
"stacked",
["omniglyph:context-as-image"],
undefined,
Date.now() - started
);
// A contabilidade só acompanha uma conversão que realmente aconteceu: um skip
// não tem economia para reportar, e inventar zeros ali viraria "0% de ganho"
// indistinguível de "a engine nem rodou".
if (accounting) stats.omniglyph = accounting;
return { body: outBody, compressed: true, stats };
}
export const omniglyphEngine: CompressionEngine = {
id: "omniglyph",
name: "OmniGlyph",
description:
"Contexto-como-imagem (Anthropic Fable 5, rota direta): system prompt, tool docs e histórico viram páginas PNG densas — ~10× menos tokens no bloco convertido.",
"Contexto-como-imagem para Claude Fable 5 na rota direta medida; wires GPT nativos ficam disponíveis apenas após recibo de fidelidade do provedor.",
icon: "image",
targets: ["messages", "tool_results"],
stackable: true,
@@ -169,7 +284,7 @@ export const omniglyphEngine: CompressionEngine = {
id: "omniglyph",
name: "OmniGlyph",
description:
"Contexto-como-imagem para Claude Fable 5 e GPT 5.6 via wires nativos Anthropic/OpenAI em rota direta.",
"Contexto-como-imagem para Claude Fable 5 na rota direta medida; transformadores GPT nativos permanecem fail-closed até validação do provedor.",
inputScope: "mixed",
targetLatencyMs: 250, // render+encode PNG de páginas grandes
supportsPreview: true,

View File

@@ -7,6 +7,9 @@ export type CompressionWireFormat = "claude" | "openai" | "openai-responses" | s
export type CompressionStage = "pre-translation" | "post-translation";
/** Whether an upstream route preserves OmniGlyph PNG bytes and dimensions. */
export type ImageTransportFidelity = "byte-preserving" | "resizes" | "unknown";
export interface EngineConfigField {
key: string;
type: "boolean" | "number" | "string" | "select" | "multiselect";
@@ -42,8 +45,11 @@ export interface CompressionEngineApplyOptions {
/** Como o request chega ao provider: rota direta oficial ('direct') vs
* agregador que pode reprocessar imagens ('aggregator'). O engine omniglyph
* exige 'direct' — medição 2026-07-06: agregadores redimensionam as páginas
* e destroem a legibilidade. undefined = desconhecido = skip (fail-closed). */
* e destroem a legibilidade. A política de produção também informa
* imageTransportFidelity; chamadas legadas sem esse campo mantêm o gate direct. */
providerTransport?: "direct" | "aggregator";
/** Independent image-fidelity gate; direct HTTP does not imply byte preservation. */
imageTransportFidelity?: ImageTransportFidelity;
/** Protocol shape before the current compression stage. */
sourceFormat?: CompressionWireFormat;
/** Protocol shape expected by the upstream provider. */
@@ -55,6 +61,10 @@ export interface CompressionEngineApplyOptions {
stepConfig?: Record<string, unknown>;
/** Authenticated principal (API key id) making the request. Used by CCR to scope its store. */
principalId?: string;
/** Provider resolvido do alvo. A contabilidade do omniglyph depende dele:
* Anthropic reporta input/cache em buckets disjuntos, OpenAI/xAI reportam
* cached como subconjunto do input. Ausente => `unknown` (falha fechado). */
provider?: string;
}
export interface CompressionEngine {

View File

@@ -0,0 +1,33 @@
/**
* Provider-level image transport policy for loss-sensitive compression engines.
*
* `supportsVision` only says that a model can read images. OmniGlyph also needs
* the PNG bytes and dimensions to survive the provider route unchanged. The
* allowlist below contains only paths with an existing OmniRoute receipt;
* everything else is deliberately classified as unknown and skipped.
*/
import type { ImageTransportFidelity } from "./engines/types.ts";
export type OmniGlyphTransportPolicy = {
providerTransport: "direct" | "aggregator";
imageTransportFidelity: ImageTransportFidelity;
};
const BYTE_PRESERVING_PROVIDERS = new Set(["anthropic", "claude"]);
export function resolveOmniGlyphTransport(
provider: string | null | undefined
): OmniGlyphTransportPolicy {
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
if (BYTE_PRESERVING_PROVIDERS.has(normalized)) {
return {
providerTransport: "direct",
imageTransportFidelity: "byte-preserving",
};
}
return {
providerTransport: "aggregator",
imageTransportFidelity: "unknown",
};
}

View File

@@ -0,0 +1,156 @@
/**
* Ponte de telemetria do OmniGlyph — allowlist positiva.
*
* `TransformInfo` mistura contadores inofensivos com material que NUNCA pode
* ser persistido: bytes PNG, `imageSourceText(s)`, `recoverable[].text`, os
* sha8 de system/CLAUDE.md/primeira mensagem, os nomes de tags observadas e o
* bloco `env` (cwd, branch, versões). Copiar o objeto inteiro seria transformar
* a telemetria de compressão num vazamento do prompt.
*
* Este módulo não filtra por denylist — ele MONTA um objeto novo, campo a
* campo, só com número e enum. Um campo novo no upstream não entra sozinho.
*
* `normalizeAccounting()` (OmniGlyph 1.4.0) faz a parte difícil: classifica o
* grau de evidência da economia e resolve a semântica de cache por provider —
* Anthropic reporta input/cache-create/cache-read em buckets DISJUNTOS,
* enquanto OpenAI e xAI reportam `cached` como SUBCONJUNTO do input. Somar à
* mão dá double-count silencioso.
*/
import {
normalizeAccounting,
type AccountingProvider,
type OmniGlyphTransformInfo,
type SavingsEvidence,
} from "omniglyph";
/** Contabilidade segura de uma execução do OmniGlyph. Só número e enum. */
export interface OmniGlyphAccounting {
provider: AccountingProvider;
model?: string;
bytes: {
original?: number;
transformed?: number;
reduced?: number;
compressionRatio?: number;
};
tokens: {
estimatedOriginalInput?: number;
estimatedActualInput?: number;
estimatedReduced?: number;
image?: number;
};
savings: {
/** De onde saiu o número: contagem do provider, estimativa ou só bytes. */
evidence: SavingsEvidence;
inputTokensReduced?: number;
inputReductionRatio?: number;
};
images: {
count: number;
bytes: number;
pixels?: number;
};
/** Chars de origem imageados vs. mantidos como texto por turno. */
chars: {
original?: number;
imaged?: number;
static?: number;
dynamic?: number;
outgoingText?: number;
};
dynamicBlockCount?: number;
latencyMs?: number;
}
/**
* A semântica de cache de `normalizeAccounting` depende da família do provider,
* não do nome comercial da rota. Rota desconhecida vira `unknown`, que faz o
* upstream falhar fechado em vez de adivinhar buckets de cache.
*/
export function toAccountingProvider(provider: string | null | undefined): AccountingProvider {
const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : "";
if (normalized === "anthropic" || normalized === "claude") return "anthropic";
if (normalized === "openai" || normalized === "codex" || normalized === "chatgpt") {
return "openai";
}
if (normalized === "xai" || normalized === "grok") return "xai";
return "unknown";
}
function count(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}
export function buildOmniGlyphAccounting(params: {
provider: string | null | undefined;
model?: string;
originalBytes: number;
transformedBytes: number;
info?: OmniGlyphTransformInfo | null;
durationMs?: number;
}): OmniGlyphAccounting {
const { info } = params;
const provider = toAccountingProvider(params.provider);
// `baselineImagedTokens` é o custo em tokens de texto do que foi imageado (o
// "teria pago assim"); `imageTokens` é o que as imagens custam de fato. Os
// dois só existem no wire GPT — no Anthropic a evidência honesta cai para
// bytes, e é isso que o campo `evidence` passa a dizer em vez de exibir um
// número sem procedência.
const normalized = normalizeAccounting({
provider,
...(params.model ? { model: params.model } : {}),
originalBytes: params.originalBytes,
transformedBytes: params.transformedBytes,
...(count(info?.baselineImagedTokens) !== undefined
? { estimatedOriginalInputTokens: info!.baselineImagedTokens }
: {}),
...(count(info?.imageTokens) !== undefined
? { estimatedTransformedInputTokens: info!.imageTokens }
: {}),
...(count(info?.imageTokens) !== undefined ? { imageTokens: info!.imageTokens } : {}),
...(params.durationMs !== undefined ? { proxyAddedLatencyMs: params.durationMs } : {}),
});
const chars = {
...(count(info?.origChars) !== undefined ? { original: info!.origChars } : {}),
...(count(info?.compressedChars) !== undefined ? { imaged: info!.compressedChars } : {}),
...(count(info?.staticChars) !== undefined ? { static: info!.staticChars } : {}),
...(count(info?.dynamicChars) !== undefined ? { dynamic: info!.dynamicChars } : {}),
...(count(info?.outgoingTextChars) !== undefined
? { outgoingText: info!.outgoingTextChars }
: {}),
};
return {
provider: normalized.provider,
...(normalized.model ? { model: normalized.model } : {}),
bytes: normalized.bytes,
tokens: {
...(normalized.tokens.estimatedOriginalInput !== undefined
? { estimatedOriginalInput: normalized.tokens.estimatedOriginalInput }
: {}),
...(normalized.tokens.estimatedActualInput !== undefined
? { estimatedActualInput: normalized.tokens.estimatedActualInput }
: {}),
...(normalized.tokens.estimatedReduced !== undefined
? { estimatedReduced: normalized.tokens.estimatedReduced }
: {}),
...(normalized.tokens.image !== undefined ? { image: normalized.tokens.image } : {}),
},
savings: normalized.savings,
images: {
count: count(info?.imageCount) ?? 0,
bytes: count(info?.imageBytes) ?? 0,
...(count(info?.imagePixels) !== undefined ? { pixels: info!.imagePixels } : {}),
},
chars,
...(count(info?.dynamicBlockCount) !== undefined
? { dynamicBlockCount: info!.dynamicBlockCount }
: {}),
...(normalized.latency.proxyAddedMs !== undefined
? { latencyMs: normalized.latency.proxyAddedMs }
: {}),
};
}

View File

@@ -110,5 +110,8 @@ export function mergeStackStep(
techniquesUsed: result.stats.techniquesUsed,
...(result.stats.rulesApplied ? { rulesApplied: result.stats.rulesApplied } : {}),
...(result.stats.durationMs !== undefined ? { durationMs: result.stats.durationMs } : {}),
// O agregado do pipeline soma tokens de todas as engines; a contabilidade
// física do omniglyph só faz sentido no passo que a produziu.
...(result.stats.omniglyph ? { omniglyph: result.stats.omniglyph } : {}),
});
}

View File

@@ -77,7 +77,7 @@ function isOpenAIResponsesPngImagePart(value: unknown): value is OpenAIResponses
}
function pngDimensionsFromDataUrl(value: string): { width: number; height: number } | null {
const marker = ",base64,";
const marker = ";base64,";
const markerIndex = value.indexOf(marker);
if (markerIndex < 0) return null;
return decodePngDimensions(value.slice(markerIndex + marker.length));

View File

@@ -11,6 +11,7 @@ import type {
CompressionEngineApplyOptions,
CompressionStage,
CompressionWireFormat,
ImageTransportFidelity,
} from "./engines/types.ts";
import { applyLiteCompression } from "./lite.ts";
import { cavemanCompress } from "./caveman.ts";
@@ -266,6 +267,7 @@ export function applyCompression(
options?: {
model?: string;
supportsVision?: boolean | null;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -292,6 +294,7 @@ function runCompression(
options?: {
model?: string;
supportsVision?: boolean | null;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -479,6 +482,9 @@ export async function applyCompressionAsync(
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
/** Provider resolvido — a contabilidade do omniglyph depende dele. */
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -501,6 +507,9 @@ async function runCompressionAsync(
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
/** Provider resolvido — a contabilidade do omniglyph depende dele. */
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -693,6 +702,9 @@ interface StackOptions {
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
/** Provider resolvido — a contabilidade do omniglyph depende dele. */
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
@@ -800,7 +812,11 @@ function canRunAtCompressionStage(
stage: CompressionStage | undefined
): boolean {
const effectiveStage = stage ?? "pre-translation";
const stages = engine.metadata.executionStages;
// `assertValidEngine` não exige `metadata`, então uma engine registrada sem
// esse campo é legal — e sem a guarda derrubava o pipeline inteiro com
// TypeError em vez de falhar aberto. Metadata ausente é o mesmo caso de "não
// declarou estágio" e cai no mesmo fallback: só pre-translation.
const stages = engine.metadata?.executionStages;
return stages ? stages.includes(effectiveStage) : effectiveStage === "pre-translation";
}

View File

@@ -16,6 +16,7 @@ import type { RiskGateConfig } from "./riskGate/riskGate.ts";
import type { PipelineCircuitBreakerConfig } from "./pipelineEngineBreaker.ts";
import type { RiskGateStats } from "./riskGate/riskGateStep.ts";
import type { QuantumLockConfig, QuantumLockStats } from "./quantumLock/quantumPatterns.ts";
import type { OmniGlyphAccounting } from "./omniglyphTelemetry.ts";
// Re-export so consumers that already import from this module (e.g. src/lib/db/compression.ts)
// can get ENGINE_IDS without a second bare `@omniroute/open-sse/...engineCatalog.ts` specifier.
@@ -157,6 +158,22 @@ export interface LiveZoneConfig {
enabled: boolean;
}
/** Perfil semântico do OmniGlyph (pacote 1.4.0+). */
export type OmniglyphProfile = "coding-safe" | "balanced" | "aggressive" | "passthrough";
/**
* Política do OmniGlyph escolhida pelo operador.
*
* O perfil é um TETO: `mergeCompressionProfileOptions` do pacote não deixa um
* override reabrir uma lane que o perfil fechou. Trocar de `aggressive` para
* `coding-safe` mantém system, schemas de tools e tool results nativos, ao custo
* medido de a engine não fazer nada até a sessão acumular histórico
* (`minCompressChars` vai ao máximo). Por isso o default é `aggressive`.
*/
export interface OmniglyphConfig {
profile: OmniglyphProfile;
}
/** Lite detail settings for proactive request-time transformations. */
export interface LiteConfig {
/** Truncate tool-result strings over 2,000 characters before provider dispatch. */
@@ -206,6 +223,8 @@ export interface CompressionConfig {
comboOverrides: Record<string, CompressionMode>;
compressionComboId?: string | null;
stackedPipeline?: CompressionPipelineStep[];
/** Política do engine OmniGlyph (perfil semântico). */
omniglyph?: OmniglyphConfig;
/** Opt-in QuantumLock cache-prefix stabilization (default off). */
quantumLock?: QuantumLockConfig;
/** Opt-in per-step fidelity gate (default disabled). */
@@ -303,6 +322,12 @@ export interface CompressionStats {
validationWarnings?: string[];
validationErrors?: string[];
fallbackApplied?: boolean;
/**
* Contabilidade física do OmniGlyph, normalizada pelo próprio pacote
* (`normalizeAccounting`). Só número e enum — ver `omniglyphTelemetry.ts`
* para a allowlist e o que nunca pode entrar aqui.
*/
omniglyph?: OmniGlyphAccounting;
riskGate?: RiskGateStats;
/**
* Phase 4 (B): which `ultra` tier actually ran for this request.
@@ -341,6 +366,8 @@ export interface CompressionStats {
durationMs?: number;
rejected?: boolean;
rejectReason?: string;
/** Contabilidade física — presente só no passo omniglyph que comprimiu. */
omniglyph?: OmniGlyphAccounting;
}>;
/** Present only when QuantumLock stabilized ≥1 fragment this run. */
quantumLock?: QuantumLockStats;
@@ -460,6 +487,16 @@ export const DEFAULT_COMPRESSION_LANGUAGE_CONFIG: CompressionLanguageConfig = {
enabledPacks: ["en"],
};
/**
* `aggressive` é a política que os recibos publicados mediram. Medido nesta
* base: com `coding-safe`/`balanced`, uma sessão sem histórico acumulado para em
* `below_min_chars` e a engine não faz nada — como o OmniGlyph é opt-in, esse
* default entregaria "ligado, 0% de ganho".
*/
export const DEFAULT_OMNIGLYPH_CONFIG: OmniglyphConfig = {
profile: "aggressive",
};
export const DEFAULT_CONTEXT_EDITING_CONFIG: ContextEditingConfig = {
enabled: false,
};

View File

@@ -49,7 +49,6 @@ const FALLBACK_MODEL_SEEDS: FallbackModelSeed[] = [
{ id: "claude-opus-5", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-opus-4-8", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-fable-5", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-opus-4-7", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-sonnet-5", vision: true, efforts: EFFORTS_NO_XHIGH },
{ id: "claude-sonnet-4-6", vision: true, efforts: EFFORTS_NO_XHIGH },
{ id: "claude-haiku-4-5", vision: true, efforts: EFFORTS_STANDARD },
@@ -61,7 +60,6 @@ const FALLBACK_MODEL_SEEDS: FallbackModelSeed[] = [
{ id: "deepseek/deepseek-v4-pro", vision: false, efforts: EFFORTS_HIGH_ONLY },
{ id: "openrouter/fusion", vision: false, efforts: [] },
{ id: "z-ai/glm-5.2", vision: false, efforts: EFFORTS_STANDARD },
{ id: "z-ai/glm-5.1", vision: false, efforts: EFFORTS_STANDARD },
{ id: "tencent/hy3", vision: false, efforts: EFFORTS_STANDARD },
{ id: "moonshotai/kimi-k3", vision: true, efforts: EFFORTS_STANDARD },
{ id: "moonshotai/kimi-k2.7-code", vision: true, efforts: EFFORTS_STANDARD },
@@ -69,7 +67,7 @@ const FALLBACK_MODEL_SEEDS: FallbackModelSeed[] = [
{ id: "qwen/qwen3.7-max", vision: false, efforts: EFFORTS_STANDARD },
{ id: "minimax/minimax-m3", vision: true, efforts: EFFORTS_STANDARD },
{ id: "stepfun/step-3.7-flash", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.5-flash", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.7-flash", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.1-pro-preview", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.1-flash-lite", vision: true, efforts: EFFORTS_STANDARD },
{ id: "x-ai/grok-4.3", vision: true, efforts: EFFORTS_STANDARD },

View File

@@ -32,7 +32,7 @@ export const GITHUB_COPILOT_MODEL_ALLOWLIST = [
"claude-sonnet-4.5",
"claude-haiku-4.5",
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
"gemini-3.7-flash",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",

View File

@@ -30,84 +30,96 @@ export type NotionDiscoveredModel = {
*/
export const NOTION_WEB_FALLBACK_MODELS: NotionDiscoveredModel[] = [
{ id: "notion-ai", name: "Notion AI (default)", owned_by: "notion" },
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", owned_by: "openai", notionCodename: "orange-mousse" },
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
owned_by: "openai",
supportsReasoning: true,
notionCodename: "orange-mousse",
},
{
id: "gpt-5.6-terra",
name: "GPT-5.6 Terra",
owned_by: "openai",
supportsReasoning: true,
notionCodename: "orchid-muffin",
},
{
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
owned_by: "openai",
supportsReasoning: true,
notionCodename: "olive-jellyroll",
},
{ id: "gpt-5.2", name: "GPT-5.2", owned_by: "openai", notionCodename: "oatmeal-cookie" },
{ id: "gpt-5.4", name: "GPT-5.4", owned_by: "openai", notionCodename: "oval-kumquat-medium" },
{ id: "gpt-5.5", name: "GPT-5.5", owned_by: "openai", notionCodename: "opal-quince-medium" },
{
id: "gpt-5.4-mini",
name: "GPT-5.4 Mini",
owned_by: "openai",
supportsReasoning: true,
notionCodename: "oregon-grape-medium",
},
{
id: "gpt-5.4-nano",
name: "GPT-5.4 Nano",
owned_by: "openai",
supportsReasoning: true,
notionCodename: "otaheite-apple-medium",
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
owned_by: "gemini",
notionCodename: "vertex-gemini-3.5-flash",
},
{
id: "gemini-3-flash",
name: "Gemini 3 Flash",
owned_by: "gemini",
notionCodename: "gingerbread",
supportsReasoning: true,
notionCodename: "grapefruit-zeppole",
},
{
id: "gemini-3.1-pro",
name: "Gemini 3.1 Pro",
owned_by: "gemini",
supportsReasoning: true,
notionCodename: "galette-medium-thinking",
},
{
id: "sonnet-4.6",
name: "Sonnet 4.6",
id: "fable-5",
name: "Claude Fable 5",
owned_by: "anthropic",
notionCodename: "almond-croissant-low",
},
{ id: "sonnet-5", name: "Sonnet 5", owned_by: "anthropic", notionCodename: "angel-cake-high" },
{
id: "opus-4.6",
name: "Opus 4.6",
owned_by: "anthropic",
notionCodename: "avocado-froyo-medium",
supportsReasoning: true,
disabled: true,
notionCodename: "acai-budino-high",
},
{
id: "opus-4.7",
name: "Opus 4.7",
id: "opus-5",
name: "Claude Opus 5",
owned_by: "anthropic",
notionCodename: "apricot-sorbet-high",
supportsReasoning: true,
notionCodename: "agave-flan",
},
{
id: "sonnet-5",
name: "Claude Sonnet 5",
owned_by: "anthropic",
supportsReasoning: true,
notionCodename: "angel-cake-high",
},
{ id: "opus-4.8", name: "Opus 4.8", owned_by: "anthropic", notionCodename: "ambrosia-tart-high" },
{
id: "haiku-4.5",
name: "Haiku 4.5",
name: "Claude Haiku 4.5",
owned_by: "anthropic",
notionCodename: "anthropic-haiku-4.5",
},
{ id: "fable-5", name: "Fable 5", owned_by: "anthropic", notionCodename: "acai-budino-high" },
{
id: "kimi-k2.6",
name: "Kimi K2.6",
id: "grok-4.6",
name: "Grok 4.6",
owned_by: "xai",
supportsReasoning: true,
notionCodename: "soursop-shortcake",
},
{
id: "kimi-k3",
name: "Kimi K3",
owned_by: "mystery",
notionCodename: "fireworks-kimi-k2.6",
supportsReasoning: true,
notionCodename: "fireworks-kimi-k3",
},
{
id: "kimi-k2.7-code",
@@ -119,16 +131,14 @@ export const NOTION_WEB_FALLBACK_MODELS: NotionDiscoveredModel[] = [
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
owned_by: "mystery",
supportsReasoning: true,
notionCodename: "baseten-deepseek-v4-pro",
},
{ id: "glm-5.2", name: "GLM 5.2", owned_by: "mystery", notionCodename: "baseten-glm-5.2" },
{ id: "grok-4.3", name: "Grok 4.3", owned_by: "xai", notionCodename: "xigua-mochi-medium" },
{ id: "grok-4.5", name: "Grok 4.5", owned_by: "xai", notionCodename: "strawberry-whoopiepie" },
{
id: "grok-build-0.1",
name: "Grok Build 0.1",
owned_by: "xai",
notionCodename: "xinomavro-cake",
id: "glm-5.2",
name: "GLM 5.2",
owned_by: "mystery",
supportsReasoning: true,
notionCodename: "baseten-glm-5.2",
},
];

View File

@@ -2,7 +2,7 @@
* PromptQL (prompt.ql.app) model catalog helpers.
*
* Live catalog: GraphQL `FetchLlmConfigs` against the playground Hasura endpoint.
* Fallback: static seed captured 2026-07-20 (display_label / model_reference / model_id).
* Fallback: static seed captured 2026-08-17 (display_label / model_reference / model_id).
*/
export interface PromptQlModel {
@@ -21,95 +21,102 @@ export interface PromptQlModel {
/** Offline seed when discovery fails (from live FetchLlmConfigs capture). */
export const PROMPTQL_FALLBACK_MODELS: PromptQlModel[] = [
{
id: "vertex-claude-fable-5",
id: "bedrock-claude-fable-5",
name: "Claude Fable 5",
configId: "967e6517-1d6b-4e22-82fb-3463bab239c4",
modelId: "anthropic/claude-fable-5",
configId: "c47a1e57-2fca-4cfe-913a-5fb821079f50",
modelId: "us.anthropic.claude-fable-5",
supportsVision: true,
},
{
id: "bedrock-claude-opus-4-8",
name: "Claude Opus 4.8",
configId: "e97e7f50-9e4a-4685-bc14-1854f1f79782",
modelId: "us.anthropic.claude-opus-4-8",
id: "bedrock-claude-opus-5",
name: "Claude Opus 5",
configId: "8aed42aa-f7c8-48f9-8238-5046bbc0f4f7",
modelId: "us.anthropic.claude-opus-5",
supportsVision: true,
},
{
id: "bedrock-claude-sonnet-4-5",
name: "Claude Sonnet 4.5",
configId: "48105d83-9a45-4ec6-8b58-f3cf44094f92",
configId: "0abcbc61-dbef-4958-96b9-e0cde7e3ad8f",
modelId: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
supportsVision: true,
},
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
configId: "5a23af33-b31b-4215-892c-20ef633a8848",
modelId: "accounts/fireworks/models/deepseek-v4-pro",
id: "deepseek-v4-pro-0813",
name: "DeepSeek V4 Pro 0813",
configId: "255de820-3615-4921-a5fe-85b4af9e37a4",
modelId: "accounts/fireworks/models/deepseek-v4-pro-0813",
supportsVision: true,
},
{
id: "deepseek-v4-flash-0731",
name: "DeepSeek V4 Flash 0731",
configId: "22d8bd9a-3c48-4e27-a4f8-dc67ad2242b7",
modelId: "accounts/fireworks/models/deepseek-v4-flash-0731",
supportsVision: true,
},
{
id: "gemini-3.1-pro-preview",
name: "Gemini 3.1 Pro Preview",
configId: "d2bda5cd-881b-4044-aeb9-02a83cc0ca27",
configId: "17703a97-41a4-469d-b5d4-7356f1c28948",
modelId: "google/gemini-3.1-pro-preview",
supportsVision: true,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
configId: "c3a25aa0-ca48-4577-b52d-71282aacb687",
modelId: "google/gemini-3.5-flash",
},
{
id: "glm-5.2",
name: "GLM 5.2",
configId: "64a1fa3d-bf2e-4bb9-8c2b-fa76c218d636",
modelId: "accounts/fireworks/models/glm-5p2",
},
{
id: "gpt-5.5",
name: "GPT 5.5",
configId: "1762fbce-d5bf-4bf4-ba3d-8b1201f8e204",
modelId: "gpt-5.5",
},
{
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
configId: "a9c45ba7-87fa-49a1-8165-76b0864c3a55",
modelId: "gpt-5.6-luna",
id: "gemini-3.7-flash",
name: "Gemini 3.7 Flash",
configId: "60754535-a5e8-4ae7-acf2-43d046771700",
modelId: "google/gemini-3.7-flash",
supportsVision: true,
},
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
configId: "34c80712-def3-4db3-9e7a-f57b0324b43d",
configId: "4914e63d-ea29-45dc-9a85-c367b1ad0be5",
modelId: "gpt-5.6-sol",
supportsVision: true,
},
{
id: "gpt-5.6-terra",
name: "GPT-5.6 Terra",
configId: "04f1a08c-42b2-4371-b6d8-75c50b9bb990",
configId: "4e627eb9-a199-4a90-8050-b734b6ee5fda",
modelId: "gpt-5.6-terra",
supportsVision: true,
},
{
id: "xai-grok-4-5",
name: "Grok 4.5",
configId: "068b2ef2-e432-422b-98e5-5863a1852c47",
modelId: "grok-4.5",
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
configId: "5eac2efb-7951-4da6-9a32-bfb31b1a7788",
modelId: "gpt-5.6-luna",
supportsVision: true,
},
{
id: "kimi-k2.6",
name: "Kimi K2.6",
configId: "placeholder-kimi-k2.6",
modelId: "accounts/fireworks/models/kimi-k2p6",
id: "xai-grok-4-6",
name: "Grok 4.6",
configId: "673e97ba-7b15-4213-8984-9b2477ee3409",
modelId: "grok-4.6",
supportsVision: true,
},
{
id: "kimi-k2.7-code",
name: "Kimi K2.7 Code",
configId: "placeholder-kimi-k2.7-code",
modelId: "accounts/fireworks/models/kimi-k2p7-code",
id: "kimi-k3",
name: "Kimi K3",
configId: "2a751e62-e281-4ab0-9be0-b05a6f8603db",
modelId: "accounts/fireworks/models/kimi-k3",
supportsVision: true,
},
{
id: "glm-5.2",
name: "GLM 5.2",
configId: "d2694d4d-4285-4d3c-ada5-aea5956375d4",
modelId: "accounts/fireworks/models/glm-5p2",
supportsVision: false,
},
{
id: "minimax-m3",
name: "Minimax M3",
configId: "placeholder-minimax-m3",
configId: "c4028069-0eb9-4a31-825c-a1cff9e5a085",
modelId: "accounts/fireworks/models/minimax-m3",
supportsVision: true,
supportsVision: false,
},
];

375
package-lock.json generated
View File

@@ -60,7 +60,7 @@
"next-intl": "^4.13.6",
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"omniglyph": "^1.0.2",
"omniglyph": "^1.4.0",
"onnxruntime-node": "~1.24.3",
"open": "^11.0.1",
"ora": "^9.4.1",
@@ -155,8 +155,7 @@
"node": ">=22.22.2 <23 || >=24.0.0 <27"
},
"optionalDependencies": {
"@atjsh/llmlingua-2": "2.0.3",
"@tensorflow/tfjs": "4.22.0",
"@atjsh/llmlingua-2": "2.0.5",
"better-sqlite3": "^13.0.2",
"js-tiktoken": "^1.0.20",
"keytar": "^7.9.0",
@@ -556,17 +555,16 @@
}
},
"node_modules/@atjsh/llmlingua-2": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@atjsh/llmlingua-2/-/llmlingua-2-2.0.3.tgz",
"integrity": "sha512-UJJFMbzYldkZ4qX5CrSZtmytOnXf6aXhmr1sBhbpVMHdmQG+7GCnrx5rIwPSOmozXD9KiPv5nnV6pvzxdtHdYQ==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@atjsh/llmlingua-2/-/llmlingua-2-2.0.5.tgz",
"integrity": "sha512-cXdGUJgx0e2Sui5gYC8kapOhw1HAxwzh9IuYPdqyB+VlP6SL9imIfyB7I4GTCl/iG+BUxaOqSrLqWsWYvDZuVQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"es-toolkit": "^1.38.0"
},
"peerDependencies": {
"@huggingface/transformers": "*",
"@tensorflow/tfjs": "*",
"@huggingface/transformers": "^3.5.2 || ^4.0.0",
"js-tiktoken": "*"
}
},
@@ -12241,241 +12239,6 @@
"tailwindcss": "4.3.3"
}
},
"node_modules/@tensorflow/tfjs": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz",
"integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@tensorflow/tfjs-backend-cpu": "4.22.0",
"@tensorflow/tfjs-backend-webgl": "4.22.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.0",
"@tensorflow/tfjs-data": "4.22.0",
"@tensorflow/tfjs-layers": "4.22.0",
"argparse": "^1.0.10",
"chalk": "^4.1.0",
"core-js": "3.29.1",
"regenerator-runtime": "^0.13.5",
"yargs": "^16.0.3"
},
"bin": {
"tfjs-custom-module": "dist/tools/custom_module/cli.js"
}
},
"node_modules/@tensorflow/tfjs-backend-cpu": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz",
"integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@types/seedrandom": "^2.4.28",
"seedrandom": "^3.0.5"
},
"engines": {
"yarn": ">= 1.3.2"
},
"peerDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
}
},
"node_modules/@tensorflow/tfjs-backend-webgl": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz",
"integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@tensorflow/tfjs-backend-cpu": "4.22.0",
"@types/offscreencanvas": "~2019.3.0",
"@types/seedrandom": "^2.4.28",
"seedrandom": "^3.0.5"
},
"engines": {
"yarn": ">= 1.3.2"
},
"peerDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
}
},
"node_modules/@tensorflow/tfjs-converter": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz",
"integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==",
"license": "Apache-2.0",
"optional": true,
"peerDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
}
},
"node_modules/@tensorflow/tfjs-core": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz",
"integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@types/long": "^4.0.1",
"@types/offscreencanvas": "~2019.7.0",
"@types/seedrandom": "^2.4.28",
"@webgpu/types": "0.1.38",
"long": "4.0.0",
"node-fetch": "~2.6.1",
"seedrandom": "^3.0.5"
},
"engines": {
"yarn": ">= 1.3.2"
}
},
"node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
"license": "MIT",
"optional": true
},
"node_modules/@tensorflow/tfjs-data": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz",
"integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@types/node-fetch": "^2.1.2",
"node-fetch": "~2.6.1",
"string_decoder": "^1.3.0"
},
"peerDependencies": {
"@tensorflow/tfjs-core": "4.22.0",
"seedrandom": "^3.0.5"
}
},
"node_modules/@tensorflow/tfjs-layers": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz",
"integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==",
"license": "Apache-2.0 AND MIT",
"optional": true,
"peerDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
}
},
"node_modules/@tensorflow/tfjs/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"optional": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/@tensorflow/tfjs/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"optional": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/@tensorflow/tfjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT",
"optional": true
},
"node_modules/@tensorflow/tfjs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@tensorflow/tfjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@tensorflow/tfjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@tensorflow/tfjs/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@tensorflow/tfjs/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"optional": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@tensorflow/tfjs/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"optional": true,
"engines": {
"node": ">=10"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz",
@@ -13022,13 +12785,6 @@
"@types/node": "*"
}
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT",
"optional": true
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -13059,24 +12815,6 @@
"undici-types": "~8.3.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.4"
}
},
"node_modules/@types/offscreencanvas": {
"version": "2019.3.0",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz",
"integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==",
"license": "MIT",
"optional": true
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -13153,13 +12891,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/seedrandom": {
"version": "2.4.34",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz",
"integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==",
"license": "MIT",
"optional": true
},
"node_modules/@types/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
@@ -13978,13 +13709,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@webgpu/types": {
"version": "0.1.38",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz",
"integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==",
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/@xmldom/xmldom": {
"version": "0.9.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
@@ -17208,18 +16932,6 @@
"node": ">=6.6.0"
}
},
"node_modules/core-js": {
"version": "3.29.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz",
"integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@@ -26774,13 +26486,6 @@
"node": ">=0.1.90"
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0",
"optional": true
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -29266,52 +28971,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
"license": "MIT",
"optional": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT",
"optional": true
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause",
"optional": true
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-forge": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
@@ -29929,9 +29588,9 @@
"license": "MIT"
},
"node_modules/omniglyph": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/omniglyph/-/omniglyph-1.3.1.tgz",
"integrity": "sha512-6QnZCoXYczjsPN2x+XpbimimjO6kCoSZUzsdSvoKjtw28U1U724VgLICBNaLX4FFs5jd7SrYNNs9Aee2iIkcoA==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/omniglyph/-/omniglyph-1.4.0.tgz",
"integrity": "sha512-4zAqDW9pBb2i+fiGOVLIKbdecZeo55UmKQoLku1apxo3TSA4gfqcMo2MqQpal1VicckUwTbexFLasW7qngXUHA==",
"license": "MIT",
"dependencies": {
"gpt-tokenizer": "^3.4.0"
@@ -33071,13 +32730,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
@@ -33916,7 +33568,7 @@
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/selfsigned": {
@@ -34761,13 +34413,6 @@
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sql.js": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.2.tgz",

View File

@@ -305,7 +305,7 @@
"next-intl": "^4.13.6",
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"omniglyph": "^1.0.2",
"omniglyph": "^1.4.0",
"open": "^11.0.1",
"ora": "^9.4.1",
"parse5": "^8.0.1",
@@ -341,8 +341,7 @@
"onnxruntime-node": "~1.24.3"
},
"optionalDependencies": {
"@atjsh/llmlingua-2": "2.0.3",
"@tensorflow/tfjs": "4.22.0",
"@atjsh/llmlingua-2": "2.0.5",
"better-sqlite3": "^13.0.2",
"js-tiktoken": "^1.0.20",
"keytar": "^7.9.0",

View File

@@ -6,7 +6,10 @@
* deployment runs `server.js` from that directory directly (not the assembled
* `dist/` bundle). The standalone trace cannot see worker_threads entrypoints
* resolved at runtime, including the required call-log artifact worker and the
* optional LLMLingua-2 worker. It also omits LLMLingua's optional dependencies.
* optional LLMLingua-2 worker (`open-sse/services/compression/engines/llmlingua/onnxWorker.js`,
* dynamically spawned via worker_threads — untraceable by webpack). It also omits
* LLMLingua's optional SLM deps (`@atjsh/llmlingua-2`, `js-tiktoken`) — they are
* optionalDependencies and are only installed at the ROOT `node_modules`.
*
* The call-log worker is required, so a bundle failure must fail the build.
* LLMLingua remains fail-soft when its optional dependencies are absent.

View File

@@ -4,31 +4,32 @@
* OmniRoute — Co-locate the LLMLingua-2 optional dependency closure into the standalone bundle.
*
* The compression "ultra" SLM tier (PR #4257) runs `@atjsh/llmlingua-2` +
* `@huggingface/transformers` + `@tensorflow/tfjs` + `js-tiktoken` inside a worker thread
* `@huggingface/transformers` + `js-tiktoken` inside a worker thread
* (`open-sse/services/compression/engines/llmlingua/onnxWorker.js`, shipped under `dist/`). These
* are `optionalDependencies`: npm installs them into the ROOT `node_modules` on
* `--include=optional`, but the Next.js standalone trace bundles ONLY `@huggingface/transformers`
* (3.5.2, pinned) into `dist/node_modules` — it does NOT trace the optional, dynamically-imported
* (4.2.0, pinned) into `dist/node_modules` — it does NOT trace the optional, dynamically-imported
* SLM packages.
*
* ## Why this matters (the instance-split bug)
*
* The worker lives under `dist/`, so its `import("@huggingface/transformers")` resolves
* `dist/node_modules/@huggingface/transformers` (3.5.2) and the worker sets the model `cacheDir`
* `dist/node_modules/@huggingface/transformers` (4.2.0) and the worker sets the model `cacheDir`
* on THAT instance's `env`. But its `import("@atjsh/llmlingua-2")` walks past `dist/node_modules`
* (no `@atjsh` there) up to the ROOT `node_modules`, and llmlingua-2's own
* `import("@huggingface/transformers")` then resolves the ROOT transformers — a DIFFERENT instance.
* The `cacheDir`/`localModelPath` config the worker set never reaches the instance llmlingua-2
* actually uses, so the local model under `DATA_DIR/models/llmlingua` is never found and the SLM
* tier silently fails-open (no compression). Worse, if the root transformers is a 4.x line,
* llmlingua-2 throws on a tokenizer-API change (`decoder.decode` is undefined).
* tier silently fails-open (no compression). (Before `@atjsh/llmlingua-2@2.0.5` a root
* transformers on the 4.x line also made llmlingua-2 throw on a tokenizer-API change
* — `decoder.decode` is undefined; 2.0.5+ supports both v3 and v4.)
*
* ## The fix
*
* Co-locate the SLM optional dependency CLOSURE from the root `node_modules` into
* `dist/node_modules` (NO-CLOBBER, so the pinned `dist` transformers 3.5.2 / onnxruntime / sharp
* `dist/node_modules` (NO-CLOBBER, so the pinned `dist` transformers 4.2.0 / onnxruntime / sharp
* stay). Then the worker resolves `@atjsh/llmlingua-2` AND `@huggingface/transformers` from the
* SAME `dist/node_modules` — a single 3.5.2 instance — so the env config applies and the local
* SAME `dist/node_modules` — a single 4.2.0 instance — so the env config applies and the local
* model loads.
*
* `@huggingface/transformers` is intentionally NOT a closure seed: it is a PEER of
@@ -54,7 +55,7 @@ import { dirname, join, sep } from "node:path";
* Entry packages of the SLM optional stack (the closure roots). `@huggingface/transformers` is
* deliberately absent — it is the pinned instance already present in `dist/node_modules`.
*/
export const SEED_PACKAGES = ["@atjsh/llmlingua-2", "@tensorflow/tfjs", "js-tiktoken"];
export const SEED_PACKAGES = ["@atjsh/llmlingua-2", "js-tiktoken"];
/**
* Compute the transitive dependency closure of `seeds` by walking each package's `dependencies` +

View File

@@ -402,7 +402,7 @@ runBuildTool(
// The worker is spawned via worker_threads at a path the Next.js bundler cannot
// statically trace, so it must ship as a standalone .js (mirrors the MCP-server
// bundling above). Heavy deps (@atjsh/llmlingua-2 / @huggingface/transformers /
// @tensorflow/tfjs / js-tiktoken) stay EXTERNAL — they are optionalDependencies,
// js-tiktoken) stay EXTERNAL — they are optionalDependencies,
// dynamically imported at runtime, and the worker fail-opens if any is absent.
const llmWorkerSrc = join(
ROOT,

View File

@@ -50,7 +50,6 @@ export const OPTIONAL_PACKS = [
{ name: "@huggingface/transformers" },
{ name: "onnxruntime-node" },
{ name: "@atjsh/llmlingua-2" },
{ name: "@tensorflow/tfjs" },
{ name: "js-tiktoken" },
],
},
@@ -156,7 +155,7 @@ export async function dirChecksum(dir) {
hash.update(String(size));
hash.update("\0");
try {
// Stream to keep memory bounded on multi-hundred-MB packages (tfjs).
// Stream to keep memory bounded on multi-hundred-MB packages (onnxruntime-node).
for await (const chunk of createReadStream(absolute)) hash.update(chunk);
} catch {
hash.update("<unreadable>");

View File

@@ -2686,7 +2686,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
};
const FREE_STACK_PRESET_MODELS = [
{ model: "agy/gemini-3.5-flash-low", weight: 0 },
{ model: "agy/gemini-3.7-flash-low", weight: 0 },
{ model: "kr/claude-sonnet-4.5", weight: 0 },
{ model: "if/kimi-k2-thinking", weight: 0 },
{ model: "if/qwen3-coder-plus", weight: 0 },

View File

@@ -17,10 +17,22 @@ import { SAMPLE_BEFORE_TEXT, SAMPLE_PAGE_PNG_DATA_URI, SAMPLE_METRICS } from "./
interface CompressionConfigLite {
engines?: Record<string, { enabled: boolean; level?: string }>;
omniglyph?: { profile?: string };
}
type EngineMap = Record<string, { enabled: boolean; level?: string }>;
/** Perfis do pacote, na ordem do mais permissivo ao mais restrito. O primeiro é
* o default: a política que os recibos publicados mediram. */
const PROFILES = [
{ id: "aggressive", key: "aggressive" },
{ id: "balanced", key: "balanced" },
{ id: "coding-safe", key: "codingSafe" },
{ id: "passthrough", key: "passthrough" },
] as const;
type ProfileId = (typeof PROFILES)[number]["id"];
/** The measured fail-closed gate chain, in evaluation order. Every no-op is telemetered
* as `skip:<reason>`; the engine only fires when all pass. */
const GATES = [
@@ -157,6 +169,40 @@ function GatesCard() {
);
}
function ProfileCard(props: {
profile: ProfileId;
disabled: boolean;
onChange: (next: ProfileId) => void;
}) {
const t = useTranslations("omniglyph");
const selected = PROFILES.find((p) => p.id === props.profile) ?? PROFILES[0];
return (
<Card className="p-6">
<div className="flex flex-col gap-3">
<h2 className="text-lg font-semibold">{t("profileTitle")}</h2>
<p className="max-w-xl text-sm text-text-muted">{t("profileDescription")}</p>
<select
className="w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm"
value={props.profile}
disabled={props.disabled}
aria-label={t("profileAria")}
data-testid="omniglyph-profile-select"
onChange={(e) => props.onChange(e.target.value as ProfileId)}
>
{PROFILES.map((p) => (
<option key={p.id} value={p.id}>
{t(`profiles.${p.key}.label`)}
</option>
))}
</select>
<span className="max-w-xl text-xs text-text-muted">
{t(`profiles.${selected.key}.description`)}
</span>
</div>
</Card>
);
}
function EnableCard(props: {
enabled: boolean;
disabled: boolean;
@@ -199,6 +245,7 @@ function EnableCard(props: {
export default function OmniglyphContextPageClient() {
const [engines, setEngines] = useState<EngineMap>({});
const [enabled, setEnabled] = useState(false);
const [profile, setProfile] = useState<ProfileId>("aggressive");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"" | "saved" | "error">("");
@@ -210,6 +257,8 @@ export default function OmniglyphContextPageClient() {
const e = data?.engines ?? {};
setEngines(e);
setEnabled(e.omniglyph?.enabled === true);
const stored = data?.omniglyph?.profile;
if (PROFILES.some((p) => p.id === stored)) setProfile(stored as ProfileId);
})
.catch(() => {})
.finally(() => setLoading(false));
@@ -245,12 +294,41 @@ export default function OmniglyphContextPageClient() {
}
};
// O perfil vive na config do engine (não no mapa `engines`), então é um PATCH
// próprio — misturá-lo no payload do toggle reescreveria o mapa inteiro.
const changeProfile = async (next: ProfileId) => {
const previous = profile;
setProfile(next);
setSaving(true);
setStatus("");
try {
const res = await fetch("/api/settings/compression", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ omniglyph: { profile: next } }),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus(""), 2000);
} else {
setProfile(previous);
setStatus("error");
}
} catch {
setProfile(previous);
setStatus("error");
} finally {
setSaving(false);
}
};
return (
<div className="mx-auto flex max-w-4xl flex-col gap-6 p-6" data-testid="omniglyph-page">
<PageHeader />
<EconomicsCard />
<BeforeAfterCard />
<GatesCard />
<ProfileCard profile={profile} disabled={loading || saving} onChange={changeProfile} />
<EnableCard
enabled={enabled}
disabled={loading || saving}

View File

@@ -20,6 +20,7 @@
import { logger } from "@omniroute/open-sse/utils/logger.ts";
import { estimateTokens } from "@omniroute/open-sse/services/contextManager.ts";
import { adaptBodyForCompression } from "@omniroute/open-sse/services/compression/bodyAdapter.ts";
import { resolveOmniGlyphTransport } from "@omniroute/open-sse/services/compression/imageTransportPolicy.ts";
import type {
CompressionConfig,
CompressionResult,
@@ -55,13 +56,16 @@ export async function applyResponsesWsCompression(
if (!enabled || !settings) return responseBody;
const adapter = adaptBodyForCompression(responseBody);
if (!adapter.adapted || !Array.isArray(adapter.body.messages) || adapter.body.messages.length === 0) {
if (
!adapter.adapted ||
!Array.isArray(adapter.body.messages) ||
adapter.body.messages.length === 0
) {
return responseBody;
}
const { selectCompressionStrategy, applyCompressionAsync } = await import(
"@omniroute/open-sse/services/compression/strategySelector.ts"
);
const { selectCompressionStrategy, applyCompressionAsync } =
await import("@omniroute/open-sse/services/compression/strategySelector.ts");
const estimatedTokens = estimateTokens(adapter.body.messages);
const cachingContext = {
@@ -83,8 +87,8 @@ export async function applyResponsesWsCompression(
const result = await applyCompressionAsync(adapter.body, mode, {
model: ctx.model,
providerTransport:
ctx.provider === "anthropic" || ctx.provider === "claude" ? "direct" : "aggregator",
...resolveOmniGlyphTransport(ctx.provider),
provider: ctx.provider,
config: settings as CompressionConfig,
cachingContext,
});

View File

@@ -1768,8 +1768,8 @@ export async function GET(
// Vertex AI lists models from the Generative Language `v1beta/models` endpoint, which both
// Express-mode API keys (via ?key=) and Service Account JSON (via a minted OAuth Bearer
// token) can reach. This surfaces the full live catalog including image models
// (imagen-*, gemini-*-image) absent from the static registry list.
// token) can reach. This surfaces the live catalog, including gemini-*-image models
// absent from the static registry list.
const credential = (apiKey || "").trim();
let queryKey: string | null = null;
let bearerToken: string | null = null;

View File

@@ -8568,13 +8568,13 @@
},
"omniglyph": {
"preview": "Preview",
"description": "Context-as-image compression. Renders the system prompt, tool documentation, and dense history as compact PNG pages that Claude Fable 5 reads instead of text. Image tokens are billed by dimensions rather than characters, so the converted block costs about 10× less. Direct Anthropic route only.",
"description": "Context-as-image compression. Renders the system prompt, tool documentation, and dense history as compact PNG pages for the measured Claude Fable 5 path. The published OmniGlyph package also exposes native GPT 5.6 transformers, but OmniRoute keeps those routes fail-closed until provider fidelity and reading receipts exist.",
"economicsTitle": "The economics",
"economics": {
"fewerTokens": "fewer tokens on the converted block",
"savings": "end-to-end savings (measured)",
"imageTokens": "image tokens for a 1568×728 page (about 28k characters)",
"accuracy": "reading accuracy on Fable 5 (n=30)"
"accuracy": "scoped Fable 5 reading receipt"
},
"beforeAfterTitle": "Before → after",
"blockSavings": "{percent}% tokens on this block",
@@ -8587,18 +8587,18 @@
"gates": {
"model": {
"label": "Model",
"pass": "claude-fable-5",
"why": "Only Fable 5 reads dense pages at 100% accuracy (measured, n=30). GPT-5.5 and Gemini 2.5 Flash are blocked."
"pass": "claude-fable-5 (measured); gpt-5.6 (HOLD)",
"why": "Only the measured Fable 5 path is enabled in OmniRoute. GPT 5.6 is structurally supported by the package but remains blocked until its provider receipt; unverified families such as Grok stay text-only."
},
"transport": {
"label": "Transport",
"pass": "direct Anthropic",
"why": "Aggregators resample images and destroy legibility — only the direct route is authoritative."
"pass": "direct provider",
"why": "Aggregators may resample images and destroy legibility — only a direct provider route is authoritative."
},
"format": {
"label": "Format",
"pass": "native Claude",
"why": "The body must use Claude format and must never put a system role inside messages."
"pass": "native Claude/OpenAI",
"why": "The body must match the provider wire: Claude Messages, OpenAI Chat Completions, or native OpenAI Responses input[]."
},
"profitable": {
"label": "Profitable",
@@ -8607,7 +8607,28 @@
}
},
"enableTitle": "Enable the engine",
"enableDescription": "Runs last in the stack (after RTK/Caveman cleans the text, OmniGlyph converts the remainder to images) and also runs standalone through <code>omniglyph</code> mode. This is a preview and remains off by default until end-to-end validation is complete.",
"enableDescription": "Runs last in the stack after text engines clean the request, using the native provider wire; OpenAI Chat/Responses requests are compressed after translation so their shape is preserved. It also runs standalone through <code>omniglyph</code> mode. This is a preview and remains off by default until end-to-end validation is complete.",
"profileTitle": "Compression profile",
"profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.",
"profiles": {
"aggressive": {
"label": "Aggressive (default)",
"description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history."
},
"balanced": {
"label": "Balanced",
"description": "Keeps live state native and protects the last 8 turns; only collapses older closed history."
},
"codingSafe": {
"label": "Coding-safe",
"description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history."
},
"passthrough": {
"label": "Passthrough",
"description": "Routes without transforming. The engine is skipped and the request is forwarded untouched."
}
},
"profileAria": "Select the OmniGlyph compression profile",
"saved": "Saved.",
"saveFailed": "Could not save.",
"enableAria": "Enable the OmniGlyph engine",

View File

@@ -8560,13 +8560,13 @@
},
"omniglyph": {
"preview": "Visualização",
"description": "Compressão de contexto como imagem. Renderiza o prompt de sistema, a documentação de ferramentas e o histórico denso como páginas PNG compactas que o Claude Fable 5 lê em vez de texto. Os tokens de imagem são cobrados por dimensões em vez de caracteres, então o bloco convertido custa cerca de 10× menos. Somente na rota direta da Anthropic.",
"description": "Compressão de contexto como imagem. Renderiza o prompt de sistema, a documentação de ferramentas e o histórico denso como páginas PNG compactas para a rota medida do Claude Fable 5. O pacote OmniGlyph também expõe transformadores nativos para GPT 5.6, mas o OmniRoute mantém essas rotas fail-closed até existirem recibos de fidelidade do provedor e de leitura.",
"economicsTitle": "A economia",
"economics": {
"fewerTokens": "menos tokens no bloco convertido",
"savings": "economia de ponta a ponta (medida)",
"imageTokens": "tokens de imagem para uma página de 1568×728 (cerca de 28 mil caracteres)",
"accuracy": "precio de leitura no Fable 5 (n=30)"
"accuracy": "recibo de leitura do Fable 5 (escopo medido)"
},
"beforeAfterTitle": "Antes → depois",
"blockSavings": "{percent}% de tokens neste bloco",
@@ -8579,18 +8579,18 @@
"gates": {
"model": {
"label": "Modelo",
"pass": "claude-fable-5",
"why": "Somente o Fable 5 lê páginas densas com 100% de precisão (medido, n=30). GPT-5.5 e Gemini 2.5 Flash são bloqueados."
"pass": "claude-fable-5 (medido); gpt-5.6 (HOLD)",
"why": "Somente a rota medida do Fable 5 está habilitada no OmniRoute. O GPT 5.6 tem suporte estrutural no pacote, mas continua bloqueado até seu recibo do provedor; famílias não verificadas, como Grok, permanecem em texto."
},
"transport": {
"label": "Transporte",
"pass": "direto Anthropic",
"why": "Agregadores reamostram imagens e destroem a legibilidade — apenas a rota direta é confiável."
"pass": "provedor direto",
"why": "Agregadores podem reamostrar imagens e destruir a legibilidade — apenas uma rota direta do provedor é confiável."
},
"format": {
"label": "Formato",
"pass": "Claude nativo",
"why": "O corpo deve usar o formato Claude e nunca colocar um papel system dentro de messages."
"pass": "Claude/OpenAI nativo",
"why": "O corpo precisa corresponder ao wire do provedor: Claude Messages, OpenAI Chat Completions ou input[] nativo do OpenAI Responses."
},
"profitable": {
"label": "Rentável",
@@ -8599,7 +8599,28 @@
}
},
"enableTitle": "Ativar a engine",
"enableDescription": "Executa por último na pilha (depois que RTK/Caveman limpa o texto, o OmniGlyph converte o restante em imagens) e também roda de forma independente pelo modo <code>omniglyph</code>. Este é um preview e permanece desativado por padrão até que a validação de ponta a ponta seja concluída.",
"enableDescription": "Executa por último na pilha, depois que os engines de texto limpam a solicitação, usando o wire nativo do provedor; solicitações OpenAI Chat/Responses são comprimidas depois da tradução para preservar seu formato. Também roda de forma independente pelo modo <code>omniglyph</code>. Este é um preview e permanece desativado por padrão até que a validação de ponta a ponta seja concluída.",
"profileTitle": "Perfil de compressão",
"profileDescription": "O perfil é um teto, não um piso: um perfil mais restrito não pode ser reaberto por um override de passo. Medido nesta base: com coding-safe ou balanced, uma sessão que ainda não acumulou histórico não comprime nada, porque os dois elevam o mínimo de caracteres e mantêm o system prompt, os schemas de tools e os tool results nativos.",
"profiles": {
"aggressive": {
"label": "Agressivo (padrão)",
"description": "A política que os recibos publicados mediram. Imageia o system prompt, a documentação de tools e o histórico denso."
},
"balanced": {
"label": "Equilibrado",
"description": "Mantém o estado vivo nativo e protege os últimos 8 turnos; só colapsa histórico antigo já fechado."
},
"codingSafe": {
"label": "Seguro para código",
"description": "Mantém autoridade, schemas de tools e saída de ferramenta nativos, protegendo os últimos 12 turnos. Não comprime nada até a sessão acumular histórico."
},
"passthrough": {
"label": "Sem transformação",
"description": "Só roteia. A engine é pulada e o request segue intacto."
}
},
"profileAria": "Selecionar o perfil de compressão do OmniGlyph",
"saved": "Salvo.",
"saveFailed": "Não foi possível salvar.",
"enableAria": "Ativar a engine OmniGlyph",

View File

@@ -8600,6 +8600,27 @@
},
"enableTitle": "Bật bộ máy",
"enableDescription": "Chạy cuối cùng trong ngăn xếp (sau khi RTK/Caveman làm sạch văn bản, OmniGlyph chuyển phần còn lại thành hình ảnh) và cũng có thể chạy độc lập qua chế độ <code>omniglyph</code>. Đây là bản xem trước và mặc định vẫn tắt cho đến khi hoàn tất kiểm thử đầu cuối.",
"profileTitle": "Hồ sơ nén",
"profileDescription": "Hồ sơ là giới hạn trên, không phải giới hạn dưới: một hồ sơ chặt chẽ hơn không thể bị mở lại bằng ghi đè ở từng bước. Đo trên chính mã nguồn này: với coding-safe hoặc balanced, một phiên chưa tích lũy lịch sử sẽ không nén gì cả, vì cả hai đều nâng ngưỡng số ký tự tối thiểu và giữ nguyên system prompt, lược đồ công cụ và kết quả công cụ.",
"profiles": {
"aggressive": {
"label": "Tích cực (mặc định)",
"description": "Chính sách mà các biên nhận đã công bố đo được. Kết xuất system prompt, tài liệu công cụ và lịch sử dày đặc thành ảnh."
},
"balanced": {
"label": "Cân bằng",
"description": "Giữ nguyên trạng thái đang hoạt động và bảo vệ 8 lượt gần nhất; chỉ gộp lịch sử cũ đã khép lại."
},
"codingSafe": {
"label": "An toàn cho lập trình",
"description": "Giữ nguyên thẩm quyền, lược đồ công cụ và đầu ra công cụ đang hoạt động, bảo vệ 12 lượt gần nhất. Không nén gì cho đến khi phiên tích lũy lịch sử."
},
"passthrough": {
"label": "Không biến đổi",
"description": "Chỉ định tuyến. Engine bị bỏ qua và yêu cầu được chuyển tiếp nguyên vẹn."
}
},
"profileAria": "Chọn hồ sơ nén của OmniGlyph",
"saved": "Đã lưu.",
"saveFailed": "Không thể lưu.",
"enableAria": "Bật bộ máy OmniGlyph",

View File

@@ -26,6 +26,8 @@ import {
DEFAULT_CODEX_RESPONSES_CONFIG,
type CodexResponsesConfig,
type ContextEditingConfig,
DEFAULT_OMNIGLYPH_CONFIG,
type OmniglyphConfig,
type EngineToggle,
type HeadroomConfig,
type McpAccessibilityConfig,
@@ -304,6 +306,19 @@ function normalizeLanguageConfig(value: unknown): CompressionLanguageConfig {
};
}
function normalizeOmniglyphConfig(value: unknown): OmniglyphConfig {
const record = toRecord(value);
const profile = record.profile;
// Um perfil desconhecido não pode virar "roda com a política padrão": cai para
// o default explícito, e o adapter ainda falha fechado se algo passar por aqui.
return {
profile:
profile === "coding-safe" || profile === "balanced" || profile === "passthrough"
? profile
: DEFAULT_OMNIGLYPH_CONFIG.profile,
};
}
function normalizeContextEditingConfig(value: unknown): ContextEditingConfig {
const record = toRecord(value);
return {
@@ -621,6 +636,7 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
...buildDetailConfigDefaults(),
contextBudget: normalizeContextBudgetConfig(undefined),
contextEditing: { ...DEFAULT_CONTEXT_EDITING_CONFIG },
omniglyph: { ...DEFAULT_OMNIGLYPH_CONFIG },
liveZone: { enabled: false },
engines: {},
activeComboId: null,
@@ -744,6 +760,9 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
case "contextEditing":
config.contextEditing = normalizeContextEditingConfig(parsed);
break;
case "omniglyph":
config.omniglyph = normalizeOmniglyphConfig(parsed);
break;
case "liveZone":
config.liveZone = { enabled: toRecord(parsed).enabled === true };
break;

View File

@@ -3,15 +3,11 @@
*
* Each model's `supportedGenerationMethods` is mapped to OmniRoute endpoints:
* - generateContent / generateAnswer → "chat"
* - predict → "images" (Imagen image generation)
* - predictLongRunning → "video" (Veo video generation)
* - embedContent → "embeddings"
* - bidiGenerateContent → "audio" (Live real-time audio)
*
* Model-id heuristics refine the long-running bucket because Google exposes both
* Imagen and Veo via long-running methods on the same endpoint:
* - id contains "veo" → ensure "video"
* - id contains "imagen" → force "images" (never "video")
* Model-id heuristics ensure Veo models remain in the video bucket.
*
* Note: `gemini-*-image` models (e.g. gemini-3-pro-image) generate images via the
* regular `generateContent` path, so they stay "chat" (image output is a chat
@@ -25,7 +21,6 @@
const METHOD_TO_ENDPOINT: Record<string, string> = {
generateContent: "chat",
embedContent: "embeddings",
predict: "images",
predictLongRunning: "video",
bidiGenerateContent: "audio",
generateAnswer: "chat",
@@ -65,15 +60,10 @@ export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] {
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
const lowerId = id.toLowerCase();
// Google exposes Imagen (image) and Veo (video) via long-running methods; the
// method alone can't always distinguish them, so refine by model id.
// Keep Veo models in the video bucket even when the method list is incomplete.
if (lowerId.includes("veo")) {
endpoints.add("video");
}
if (lowerId.includes("imagen")) {
endpoints.delete("video");
endpoints.add("images");
}
if (endpoints.size === 0) endpoints.add("chat");

View File

@@ -100,7 +100,10 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
baseUrlSupport: "none",
defaultCommand: "zcode",
notes: [
{ type: "info", text: "Uses the local ZCode app-server and its existing builtin:zai-coding-plan login." },
{
type: "info",
text: "Uses the local ZCode app-server and its existing builtin:zai-coding-plan login.",
},
{ type: "warning", text: "The response is buffered until the ZCode turn completes." },
],
},
@@ -239,29 +242,21 @@ export const CLI_TOOLS: Record<string, CliCatalogEntry> = {
acpSpawnable: false,
baseUrlSupport: "none",
modelAliases: [
"gemini-3.7-flash",
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3.7-flash-high",
"gemini-3.7-flash-medium",
"gemini-3.7-flash-low",
"claude-opus-4-6-thinking",
"claude-sonnet-4-6",
"gemini-pro-agent",
"gemini-3.1-pro-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-low",
"gemini-3.5-flash-extra-low",
"gpt-oss-120b-medium",
],
defaultModels: [
createCliModel("gemini-3.7-flash", "Gemini 3.7 Flash"),
createCliModel("gemini-3.6-flash-high", "Gemini 3.6 Flash High"),
createCliModel("gemini-3.6-flash-medium", "Gemini 3.6 Flash Medium"),
createCliModel("gemini-3.6-flash-low", "Gemini 3.6 Flash Low"),
createCliModel("gemini-3.7-flash-high", "Gemini 3.7 Flash High"),
createCliModel("gemini-3.7-flash-medium", "Gemini 3.7 Flash Medium"),
createCliModel("gemini-3.7-flash-low", "Gemini 3.7 Flash Low"),
createCliModel("gemini-pro-agent", "Gemini 3.1 Pro High"),
createCliModel("gemini-3.1-pro-low", "Gemini 3.1 Pro Low"),
createCliModel("gemini-3-flash-agent", "Gemini 3.5 Flash High"),
createCliModel("gemini-3.5-flash-low", "Gemini 3.5 Flash Medium"),
createCliModel("gemini-3.5-flash-extra-low", "Gemini 3.5 Flash Low"),
createCliModel("claude-sonnet-4-6", "Claude Sonnet 4.6"),
createCliModel("claude-opus-4-6-thinking", "Claude Opus 4.6 Thinking"),
createCliModel("gpt-oss-120b-medium", "GPT OSS 120B Medium"),

View File

@@ -153,7 +153,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: ["openai/gpt-4o"],
},
// ── Gemini 2.5 and 3.5 Flash series ──────────────────────────────
// ── Gemini 2.5 and provider-neutral 3.5 Flash series ─────────────
"gemini-2.5-flash": {
maxOutputTokens: 65536,
contextWindow: 1048576,
@@ -174,24 +174,17 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
thinkingBudgetCap: 0,
},
// ── Gemini 3.7 Flash (Antigravity) — collapsed live id ──────────
// Upstream (fetchAvailableModels on daily-cloudcode-pa) also serves this model as a
// single `gemini-3.7-flash-tiered` id via the `gemini-3.7-flash` alias in
// antigravityModelAliases.ts. Registered independently of the suffixed tier ids below
// (#3696 uniqueness invariant: each public id resolves to a distinct upstream id).
"gemini-3.7-flash": {
...GEMINI_35_FLASH_MODEL_SPEC,
defaultThinkingBudget: 8192,
thinkingBudgetCap: 65536,
supportsThinking: true,
},
// ── Gemini 3.7 / 3.6 Flash (Antigravity live tiers) ─────────────
// ── Gemini 3.7 Flash (current Antigravity/AGY live tiers) ─────────
// The model id itself selects the upstream 10k/4k/1k reasoning tier. Antigravity
// still rejects client-supplied thinking parameters, so keep the explicit-parameter
// capability aligned with the existing Gemini 3.5 tier ids.
// capability aligned with the existing Gemini Flash tier ids.
"gemini-3.7-flash-high": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.7-flash-medium": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.7-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
// Provider-neutral compatibility for providers that still serve Gemini 3.6.
// Antigravity/AGY availability is governed by their own provider catalogs and
// retirement filters; these shared specs must not be treated as an allowlist.
"gemini-3.6-flash-high": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-medium": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },

View File

@@ -3,6 +3,7 @@
* Pure data; merged by default-pricing.ts via spread (god-file decomposition; semantic split).
*/
import {
GEMINI_3_7_FLASH_PROMO_PRICING,
GPT_5_5_PRICING,
GPT_5_6_LUNA_PRICING,
GPT_5_6_SOL_PRICING,
@@ -226,6 +227,7 @@ export const DEFAULT_PRICING_FRONTIER = {
"claude-opus-4": CLAUDE_OPUS_4_PRICING,
},
gemini: {
"gemini-3.7-flash": GEMINI_3_7_FLASH_PROMO_PRICING,
// Gemini 3.1 Pro — novo flagship Google (2026-03-17)
// Context: 1.050.000 tokens | Max Output: 65.536
"gemini-3.1-pro": {

View File

@@ -140,12 +140,13 @@ export const DEFAULT_PRICING_INFERENCE = {
reasoning: 1.75,
cache_creation: 0.0583,
},
"gemini-3-5-flash": {
input: 1.05,
output: 6.3,
cached: 0.1085,
reasoning: 6.3,
cache_creation: 0.0602,
// Live public catalog rate checked 2026-08-16.
"gemini-3.7-flash": {
input: 0.525,
output: 2.625,
cached: 0.13125,
reasoning: 2.625,
cache_creation: 0.017708,
},
"gemini-3-flash-preview": {
input: 0.35,
@@ -218,7 +219,13 @@ export const DEFAULT_PRICING_INFERENCE = {
reasoning: 21.0,
cache_creation: 3.5,
},
"gpt-5.6-terra": { input: 1.0, output: 6.0, cached: 0.125, reasoning: 6.0, cache_creation: 1.0 },
"gpt-5.6-terra": {
input: 1.0,
output: 6.0,
cached: 0.125,
reasoning: 6.0,
cache_creation: 1.0,
},
"grok-4.5": { input: 1.4, output: 4.2, cached: 0.35, reasoning: 4.2, cache_creation: 1.4 },
"kimi-k3": { input: 2.1, output: 10.5, cached: 0.2625, reasoning: 10.5, cache_creation: 2.1 },
"minimax-m2.7": {
@@ -229,6 +236,15 @@ export const DEFAULT_PRICING_INFERENCE = {
cache_creation: 0.168,
},
},
orcarouter: {
"google/gemini-3.6-flash": {
input: 1.5,
output: 7.5,
cached: 0.15,
reasoning: 7.5,
cache_creation: 1.5,
},
},
synthetic: {
"hf:openai/gpt-oss-120b": {
input: 0.1,

View File

@@ -4,6 +4,7 @@
*/
import {
CLAUDE_OPUS_5_PRICING,
GEMINI_3_7_FLASH_PROMO_PRICING,
GPT_5_3_CODEX_PRICING,
GPT_5_5_PRICING,
GPT_5_6_LUNA_PRICING,
@@ -11,6 +12,12 @@ import {
GPT_5_6_TERRA_PRICING,
} from "./shared-tiers";
const ANTIGRAVITY_GEMINI_3_7_PRICING = {
"gemini-3.7-flash-low": GEMINI_3_7_FLASH_PROMO_PRICING,
"gemini-3.7-flash-medium": GEMINI_3_7_FLASH_PROMO_PRICING,
"gemini-3.7-flash-high": GEMINI_3_7_FLASH_PROMO_PRICING,
};
export const DEFAULT_PRICING_OAUTH = {
cc: {
"claude-fable-5": {
@@ -277,35 +284,6 @@ export const DEFAULT_PRICING_OAUTH = {
reasoning: 18.0,
cache_creation: 2.0,
},
// Antigravity 2.0.4+ exposes Gemini 3.5 Flash as three public client ids
// (see ANTIGRAVITY_PUBLIC_MODELS in open-sse/config/antigravityModelAliases.ts):
// gemini-3.5-flash-extra-low → "Gemini 3.5 Flash (Low)"
// gemini-3-flash-agent → "Gemini 3.5 Flash (High)"
// gemini-3.5-flash-low → "Gemini 3.5 Flash (Medium)"
// Without these rows, getPricingForModel("ag", id) returns null and downstream
// cost and quota calculations silently fall back to $0.
"gemini-3.5-flash-extra-low": {
input: 0.5,
output: 3.0,
cached: 0.03,
reasoning: 4.5,
cache_creation: 0.5,
},
"gemini-3-flash-agent": {
input: 0.5,
output: 3.0,
cached: 0.03,
reasoning: 4.5,
cache_creation: 0.5,
},
"gemini-3.5-flash-low": {
input: 0.5,
output: 3.0,
cached: 0.03,
reasoning: 4.5,
cache_creation: 0.5,
},
// `gemini-pro-agent` is the callable Antigravity id for Gemini 3.1 Pro (High).
"gemini-pro-agent": {
input: 4.0,
output: 18.0,
@@ -313,40 +291,7 @@ export const DEFAULT_PRICING_OAUTH = {
reasoning: 27.0,
cache_creation: 4.0,
},
// Gemini 3.6 Flash (released 2026-07-21) - three tier variants like 3.5 Flash
// (see ANTIGRAVITY_PUBLIC_MODELS / MODEL_SPECS which already carry the catalog
// entries). Without these rows, getPricingForModel("ag", id) returns null and
// downstream cost and quota calculations silently fall back to $0.
// Pricing: $1.50 input / $7.50 output / $0.15 cached per MTok. Thinking tokens
// billed at output rate.
"gemini-3.7-flash": {
input: 1.5,
output: 7.5,
cached: 0.15,
reasoning: 7.5,
cache_creation: 1.5,
},
"gemini-3.6-flash-low": {
input: 1.5,
output: 7.5,
cached: 0.15,
reasoning: 7.5,
cache_creation: 1.5,
},
"gemini-3.6-flash-medium": {
input: 1.5,
output: 7.5,
cached: 0.15,
reasoning: 7.5,
cache_creation: 1.5,
},
"gemini-3.6-flash-high": {
input: 1.5,
output: 7.5,
cached: 0.15,
reasoning: 7.5,
cache_creation: 1.5,
},
...ANTIGRAVITY_GEMINI_3_7_PRICING,
"claude-sonnet-4-6": {
input: 3.0,
output: 15.0,
@@ -369,6 +314,8 @@ export const DEFAULT_PRICING_OAUTH = {
cache_creation: 0.5,
},
},
antigravity: ANTIGRAVITY_GEMINI_3_7_PRICING,
agy: ANTIGRAVITY_GEMINI_3_7_PRICING,
gh: {
"claude-opus-5": CLAUDE_OPUS_5_PRICING,
"gpt-5": {
@@ -441,6 +388,7 @@ export const DEFAULT_PRICING_OAUTH = {
reasoning: 4.5,
cache_creation: 0.5,
},
"gemini-3.7-flash": GEMINI_3_7_FLASH_PROMO_PRICING,
"gemini-2.5-pro": {
input: 2.0,
output: 12.0,

View File

@@ -41,6 +41,17 @@ export const GPT_5_6_LUNA_PRICING = {
cache_creation: 1.25,
};
// Gemini 3.7 Flash introductory pricing through 2026-12-31. Google and
// GitHub Copilot both publish the same current token rates; standard pricing
// becomes $1.50/$7.50/$0.15 (input/output/cached input) on 2027-01-01.
export const GEMINI_3_7_FLASH_PROMO_PRICING = {
input: 0.75,
output: 3.75,
cached: 0.075,
reasoning: 3.75,
cache_creation: 0.75,
};
export const CLAUDE_FABLE_5_PRICING = {
input: 15.0,
output: 75.0,

View File

@@ -346,6 +346,19 @@ export const contextBudgetConfigSchema = z
})
.strict();
/**
* Perfil semântico do OmniGlyph. O perfil é um TETO: o pacote não deixa um
* override reabrir uma lane que o perfil fechou. `aggressive` é o default e a
* política que os recibos publicados mediram; `coding-safe`/`balanced` mantêm
* system, schemas de tools e tool results nativos, e não comprimem nada até a
* sessão acumular histórico.
*/
export const omniglyphConfigSchema = z
.object({
profile: z.enum(["coding-safe", "balanced", "aggressive", "passthrough"]),
})
.strict();
export const compressionSettingsUpdateSchema = z
.object({
enabled: z.boolean().optional(),
@@ -373,6 +386,7 @@ export const compressionSettingsUpdateSchema = z
ccr: ccrConfigSchema.optional(),
contextBudget: contextBudgetConfigSchema.optional(),
contextEditing: contextEditingConfigSchema.optional(),
omniglyph: omniglyphConfigSchema.optional(),
liveZone: z.object({ enabled: z.boolean() }).strict().optional(),
engines: z.record(z.string(), engineToggleSchema).optional(),
enginesExplicit: z.boolean().optional(),

View File

@@ -127,7 +127,7 @@ test("checkFallbackError locks Antigravity quota-reached 429 for the full reset
429,
message,
0,
"gemini-3-flash-agent",
"gemini-3.7-flash-high",
"antigravity",
null,
makeProfile({ useUpstreamRetryHints: true })
@@ -143,7 +143,7 @@ test("checkFallbackError locks Antigravity quota-reached 429 for the full reset
test("recordModelLockoutFailure honors a multi-day exactCooldownMs (under 30-day cap)", () => {
const provider = "antigravity";
const connectionId = "conn-quota-window";
const model = "gemini-3-flash-agent";
const model = "gemini-3.7-flash-high";
const exactCooldownMs = (164 * 3600 + 27 * 60 + 24) * 1000;
clearModelLock(provider, connectionId, model);

View File

@@ -51,12 +51,15 @@ test("agy ships its own live callable model catalog", () => {
const ids = REGISTRY.agy.models.map((m) => m.id);
assert.ok(ids.includes("claude-opus-4-6-thinking"), "must expose Claude Opus 4.6 Thinking");
assert.ok(ids.includes("claude-sonnet-4-6"), "must expose Claude Sonnet 4.6");
assert.ok(ids.includes("gemini-3.6-flash-low"), "must expose Gemini 3.6 Flash Low");
assert.ok(ids.includes("gemini-3.6-flash-medium"), "must expose Gemini 3.6 Flash Medium");
assert.ok(ids.includes("gemini-3.6-flash-high"), "must expose Gemini 3.6 Flash High");
assert.ok(ids.includes("gemini-3.5-flash-extra-low"), "must expose upstream Flash Low tier");
assert.ok(ids.includes("gemini-3.5-flash-low"), "must expose upstream Flash Medium tier");
assert.ok(ids.includes("gemini-3-flash-agent"), "must expose upstream Flash High tier");
assert.ok(ids.includes("gemini-3.7-flash-low"), "must expose Gemini 3.7 Flash Low");
assert.ok(ids.includes("gemini-3.7-flash-medium"), "must expose Gemini 3.7 Flash Medium");
assert.ok(ids.includes("gemini-3.7-flash-high"), "must expose Gemini 3.7 Flash High");
assert.ok(!ids.includes("gemini-3.6-flash-low"));
assert.ok(!ids.includes("gemini-3.6-flash-medium"));
assert.ok(!ids.includes("gemini-3.6-flash-high"));
assert.ok(!ids.includes("gemini-3.5-flash-extra-low"));
assert.ok(!ids.includes("gemini-3.5-flash-low"));
assert.ok(!ids.includes("gemini-3-flash-agent"));
assert.ok(!ids.includes("gemini-3.5-flash-medium"));
assert.ok(!ids.includes("gemini-3.5-flash-high"));
assert.ok(!ids.includes("gemini-3.5-flash-preview"));
@@ -64,8 +67,8 @@ test("agy ships its own live callable model catalog", () => {
assert.ok(!ids.includes("gemini-3.1-pro-high"), "must not expose rejected Pro High id");
assert.ok(ids.includes("gemini-pro-agent"), "must expose callable Pro High id");
assert.ok(!ids.includes("gemini-2.5-pro"), "must not expose unavailable Gemini 2.5 Pro");
assert.ok(ids.includes("gemini-2.5-flash"), "must retain working Gemini 2.5 Flash");
assert.ok(ids.includes("gemini-2.5-flash-lite"), "must retain working Gemini 2.5 Flash Lite");
assert.ok(!ids.includes("gemini-2.5-flash"));
assert.ok(!ids.includes("gemini-2.5-flash-lite"));
// Tab-completion models are not chat-callable and must be excluded.
assert.ok(!ids.includes("tab_flash_lite_preview"));
assert.ok(!ids.includes("tab_jump_flash_lite_preview"));
@@ -75,15 +78,18 @@ test("agy ships its own live callable model catalog", () => {
test("agy model helpers resolve catalog ids and display names", () => {
assert.equal(isUserCallableAgyModelId("claude-opus-4-6-thinking"), true);
assert.equal(isUserCallableAgyModelId("gemini-2.5-pro"), false);
assert.equal(isUserCallableAgyModelId("gemini-2.5-flash"), true);
assert.equal(isUserCallableAgyModelId("gemini-2.5-flash"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.1-pro-high"), false);
assert.equal(isUserCallableAgyModelId("gemini-pro-agent"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-low"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-medium"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-high"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-extra-low"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-low"), true);
assert.equal(isUserCallableAgyModelId("gemini-3-flash-agent"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.7-flash-low"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.7-flash-medium"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.7-flash-high"), true);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-low"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-medium"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-high"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-extra-low"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-low"), false);
assert.equal(isUserCallableAgyModelId("gemini-3-flash-agent"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-medium"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-high"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-preview"), false);
@@ -94,23 +100,20 @@ test("agy model helpers resolve catalog ids and display names", () => {
"Claude Opus 4.6 (Thinking)"
);
assert.equal(getClientVisibleAgyModelName("gemini-pro-agent"), "Gemini 3.1 Pro (High)");
assert.equal(getClientVisibleAgyModelName("gemini-3.7-flash-low"), "Gemini 3.7 Flash (Low)");
assert.equal(
getClientVisibleAgyModelName("gemini-3.5-flash-extra-low"),
"Gemini 3.5 Flash (Low)"
getClientVisibleAgyModelName("gemini-3.7-flash-medium"),
"Gemini 3.7 Flash (Medium)"
);
assert.equal(getClientVisibleAgyModelName("gemini-3.6-flash-low"), "Gemini 3.6 Flash (Low)");
assert.equal(
getClientVisibleAgyModelName("gemini-3.6-flash-medium"),
"Gemini 3.6 Flash (Medium)"
);
assert.equal(getClientVisibleAgyModelName("gemini-3.6-flash-high"), "Gemini 3.6 Flash (High)");
assert.equal(getClientVisibleAgyModelName("gemini-3.5-flash-low"), "Gemini 3.5 Flash (Medium)");
assert.equal(getClientVisibleAgyModelName("gemini-3-flash-agent"), "Gemini 3.5 Flash (High)");
assert.equal(getClientVisibleAgyModelName("gemini-3.7-flash-high"), "Gemini 3.7 Flash (High)");
assert.equal(getClientVisibleAgyModelName("unknown-model", "Fallback"), "Fallback");
});
test("agy live discovery accepts new chat models while excluding tab-completion models", () => {
assert.equal(isDiscoverableAgyModelId("gemini-new-live-tier"), true);
assert.equal(isDiscoverableAgyModelId("gemini-3.6-flash-high"), false);
assert.equal(isDiscoverableAgyModelId("gemini-3-flash-agent"), false);
assert.equal(isDiscoverableAgyModelId("gemini-2.5-flash"), false);
assert.equal(isDiscoverableAgyModelId("tab_flash_lite_preview"), false);
assert.equal(isDiscoverableAgyModelId("tab_jump_flash_lite_preview"), false);
assert.equal(isDiscoverableAgyModelId(""), false);

View File

@@ -18,7 +18,7 @@ test("getUsageForProvider routes agy through the Antigravity usage implementatio
new Response(
JSON.stringify({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: 0.75,
resetTime: "2026-06-06T00:00:00Z",
@@ -48,7 +48,7 @@ test("getUsageForProvider routes agy through the Antigravity usage implementatio
);
assert.ok("quotas" in result, "agy should return quota data when upstream responds");
const quota = (result as { quotas: Record<string, any> }).quotas["gemini-3-flash-agent"];
const quota = (result as { quotas: Record<string, any> }).quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should expose the upstream agy per-model quota");
assert.equal(quota.remainingPercentage, 75);
assert.equal(
@@ -65,7 +65,7 @@ test("parseQuotaData treats agy quota payloads like Antigravity", () => {
const parsed = providerLimitUtils.parseQuotaData("agy", {
quotas: {
credits: { remaining: 42 },
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
used: 250,
total: 1000,
remainingPercentage: 75,
@@ -83,7 +83,7 @@ test("parseQuotaData treats agy quota payloads like Antigravity", () => {
assert.ok(credits, "credits quota should be rendered");
assert.equal(credits.isCredits, true);
const modelQuota = parsed.find((quota: any) => quota.name === "gemini-3-flash-agent");
const modelQuota = parsed.find((quota: any) => quota.name === "gemini-3.7-flash-high");
assert.ok(modelQuota, "model quota should be rendered");
assert.equal(modelQuota.remainingPercentage, 75);
});

View File

@@ -7,7 +7,7 @@
* the fallback queries and asserts the flip — the regression guard for the id contract.
*
* Contract note: the fallback queries `usage_history WHERE model = <public model id>`
* (e.g. gemini-3-flash-agent), so the executor MUST log usage under that same model id
* (e.g. gemini-3.7-flash-high), so the executor MUST log usage under that same model id
* for the fallback to fire. This test pins exactly that join.
*/
import test from "node:test";
@@ -47,7 +47,7 @@ test("Antigravity fetchAvailableModels(used=0) → localUsageHistory when usage_
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, tokens_reasoning, success, timestamp)
VALUES (?, ?, ?, ?, ?, ?, 1, ?)`
).run("antigravity", "gemini-3-flash-agent", "conn-local-1", 1000, 1500, 500, seededTimestamp);
).run("antigravity", "gemini-3.7-flash-high", "conn-local-1", 1000, 1500, 500, seededTimestamp);
// Total seeded tokens = 3000 → ceil(3000/1000) = 3 units used.
globalThis.fetch = (async (input: any) => {
@@ -61,7 +61,7 @@ test("Antigravity fetchAvailableModels(used=0) → localUsageHistory when usage_
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: { remainingFraction: 1.0, resetTime },
},
},
@@ -79,8 +79,8 @@ test("Antigravity fetchAvailableModels(used=0) → localUsageHistory when usage_
const result = await getUsageForProvider(connection, { forceRefresh: true });
assert.ok(result && "quotas" in result, "should return quotas");
const quota = (result as any).quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have the gemini-3-flash-agent quota");
const quota = (result as any).quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have the gemini-3.7-flash-high quota");
assert.equal(quota.quotaSource, "localUsageHistory", "stale full bucket replaced by local usage");
assert.equal(quota.used, 3, "3000 seeded tokens → 3 units used");
});
@@ -99,7 +99,7 @@ test("Antigravity stays fetchAvailableModels when usage_history has no matching
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": { quotaInfo: { remainingFraction: 1.0, resetTime } },
"gemini-3.7-flash-high": { quotaInfo: { remainingFraction: 1.0, resetTime } },
},
}),
} as Response;
@@ -114,7 +114,7 @@ test("Antigravity stays fetchAvailableModels when usage_history has no matching
};
const result = await getUsageForProvider(connection, { forceRefresh: true });
const quota = (result as any).quotas["gemini-3-flash-agent"];
const quota = (result as any).quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have the quota");
assert.equal(quota.quotaSource, "fetchAvailableModels", "no local rows → keep the catalog view");
assert.equal(quota.used, 0, "full bucket stays at 0 used");

View File

@@ -21,15 +21,16 @@ const EXPECTED_FLASH_TIERS = [
["gemini-3.7-flash", "Gemini 3.7 Flash"],
["gemini-3.7-flash-high", "Gemini 3.7 Flash (High)"],
["gemini-3.7-flash-medium", "Gemini 3.7 Flash (Medium)"],
["gemini-3.6-flash-low", "Gemini 3.6 Flash (Low)"],
["gemini-3.6-flash-medium", "Gemini 3.6 Flash (Medium)"],
["gemini-3.6-flash-high", "Gemini 3.6 Flash (High)"],
["gemini-3.5-flash-extra-low", "Gemini 3.5 Flash (Low)"],
["gemini-3.5-flash-low", "Gemini 3.5 Flash (Medium)"],
["gemini-3-flash-agent", "Gemini 3.5 Flash (High)"],
["gemini-3.7-flash-low", "Gemini 3.7 Flash (Low)"],
] as const;
const RETIRED_FLASH_IDS = [
"gemini-3.6-flash-low",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-high",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-medium",
"gemini-3.5-flash-high",
"gemini-3.5-flash-preview",
@@ -44,6 +45,9 @@ test("toClientAntigravityQuotaModelId preserves upstream Gemini Flash bucket IDs
// Retired preview buckets are dropped (hidden from clients).
assert.equal(toClientAntigravityQuotaModelId("gemini-3.5-flash-preview"), null);
assert.equal(toClientAntigravityQuotaModelId("gemini-3-flash-preview"), null);
for (const retiredId of RETIRED_FLASH_IDS) {
assert.equal(toClientAntigravityQuotaModelId(retiredId), null);
}
assert.equal(toClientAntigravityQuotaModelId(""), null);
});
@@ -86,9 +90,9 @@ test("isUserCallableAntigravityModelId only allows public chat-capable model IDs
}
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-flash-lite"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-pro"), false);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-lite"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-thinking"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash"), false);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-lite"), false);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-thinking"), false);
assert.equal(isUserCallableAntigravityModelId("gemini-pro-agent"), true);
// #3184: Claude IS user-callable through the Antigravity OAuth provider (same backend as
// `agy`, verified empirically). An earlier assumption that it was removed in Antigravity
@@ -109,6 +113,10 @@ test("isDiscoverableAntigravityModelId accepts new live chat models without a st
assert.equal(isDiscoverableAntigravityModelId("claude-sonnet-5"), true);
assert.equal(isDiscoverableAntigravityModelId("gemini-new-live-tier"), true);
for (const retiredId of RETIRED_FLASH_IDS) {
assert.equal(isDiscoverableAntigravityModelId(retiredId), false);
}
assert.equal(isDiscoverableAntigravityModelId("tab_flash_lite_preview"), false);
assert.equal(isDiscoverableAntigravityModelId("gemini-3.1-flash-image"), false);
assert.equal(isDiscoverableAntigravityModelId("gemini-3.1-flash-tts-preview"), false);
@@ -150,15 +158,6 @@ test("ANTIGRAVITY_PUBLIC_MODELS exposes current live names and capabilities", ()
for (const retiredId of RETIRED_FLASH_IDS) {
assert.equal(getPublicModel(retiredId), undefined);
}
assert.equal(getClientVisibleAntigravityModelName("gemini-2.5-flash"), "Gemini 2.5 Flash");
assert.equal(
getClientVisibleAntigravityModelName("gemini-2.5-flash-lite"),
"Gemini 2.5 Flash Lite"
);
assert.equal(
getClientVisibleAntigravityModelName("gemini-2.5-flash-thinking"),
"Gemini 2.5 Flash Thinking"
);
assert.deepEqual(getPublicModel("gpt-oss-120b-medium"), {
id: "gpt-oss-120b-medium",
name: "GPT-OSS 120B (Medium)",

View File

@@ -7,7 +7,7 @@
* part (usually carrying a `thoughtSignature`), so any tools request collected
* to an empty stream and was rewritten into a synthetic 502 "Provider returned
* empty content" — breaking Chatwit Captain Copilot / reply suggestions on
* agy/gemini-3.5-flash-low while plain text completions kept working.
* agy/gemini-3.7-flash-low while plain text completions kept working.
*/
import test from "node:test";
import assert from "node:assert/strict";

View File

@@ -82,6 +82,21 @@ test("an unknown model id falls back to the conservative ceiling", () => {
assert.equal(clampFor("no-such-model-xyz", 65535), MAX_ANTIGRAVITY_OUTPUT_TOKENS);
});
test("retired Antigravity Flash ids cannot inherit provider-neutral output caps", () => {
for (const modelId of [
"gemini-3.6-flash-low",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-high",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-medium",
"gemini-3.5-flash-high",
"gemini-3-flash-agent",
]) {
assert.equal(clampFor(modelId, 65536), MAX_ANTIGRAVITY_OUTPUT_TOKENS, modelId);
}
});
test("a missing or empty model id falls back too", () => {
assert.equal(clampFor(undefined, 65535), MAX_ANTIGRAVITY_OUTPUT_TOKENS);
assert.equal(clampFor(null, 65535), MAX_ANTIGRAVITY_OUTPUT_TOKENS);
@@ -211,7 +226,7 @@ test("a provider-prefixed model id resolves to the model's ceiling, not the fall
const cases: Array<[string, number]> = [
["agy/gemini-3.1-pro-high", 65535],
["antigravity/gemini-3.1-pro-high", 65535],
["agy/gemini-3.6-flash-high", 65536],
["agy/gemini-3.7-flash-high", 65536],
["agy/gpt-oss-120b-medium", 32768],
];

View File

@@ -89,7 +89,7 @@ test("#8965: quota reads use the runtime host (daily-cloudcode-pa), not cloudcod
json: async () => ({
buckets: [
{
modelId: "gemini-3-flash-agent",
modelId: "gemini-3.7-flash-high",
remainingFraction: 0.4,
resetTime: RESET_IN_2_HOURS,
},
@@ -103,10 +103,10 @@ test("#8965: quota reads use the runtime host (daily-cloudcode-pa), not cloudcod
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: { remainingFraction: 1.0, resetTime: RESET_IN_2_HOURS },
},
"gemini-3.5-flash-low": {
"gemini-3.7-flash-medium": {
quotaInfo: { remainingFraction: 0.8, resetTime: RESET_IN_2_HOURS },
},
},
@@ -148,9 +148,9 @@ test("#8965: quota reads use the runtime host (daily-cloudcode-pa), not cloudcod
// The per-model quota should come from retrieveUserQuota (the live source),
// NOT fetchAvailableModels (the stale catalog fallback).
assert.ok(quotas["gemini-3-flash-agent"], "gemini-3-flash-agent quota present");
assert.ok(quotas["gemini-3.7-flash-high"], "gemini-3.7-flash-high quota present");
assert.equal(
quotas["gemini-3-flash-agent"].quotaSource,
quotas["gemini-3.7-flash-high"].quotaSource,
"retrieveUserQuota",
"quota source is retrieveUserQuota (live), not fetchAvailableModels"
);
@@ -197,7 +197,7 @@ test("#8965 behavioral impact: live quota source + weekly bucket unreachable whe
json: async () => ({
buckets: [
{
modelId: "gemini-3-flash-agent",
modelId: "gemini-3.7-flash-high",
remainingFraction: 0.4,
resetTime: RESET_IN_2_HOURS,
},
@@ -211,7 +211,7 @@ test("#8965 behavioral impact: live quota source + weekly bucket unreachable whe
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: { remainingFraction: 1.0, resetTime: RESET_IN_2_HOURS },
},
},
@@ -250,9 +250,9 @@ test("#8965 behavioral impact: live quota source + weekly bucket unreachable whe
const quotas = (result as UsageResult).quotas;
// The per-model quota MUST come from retrieveUserQuota — the live signal.
assert.ok(quotas["gemini-3-flash-agent"], "gemini-3-flash-agent quota present");
assert.ok(quotas["gemini-3.7-flash-high"], "gemini-3.7-flash-high quota present");
assert.equal(
quotas["gemini-3-flash-agent"].quotaSource,
quotas["gemini-3.7-flash-high"].quotaSource,
"retrieveUserQuota",
"quota source is retrieveUserQuota (live), not fetchAvailableModels"
);
@@ -260,4 +260,4 @@ test("#8965 behavioral impact: live quota source + weekly bucket unreachable whe
// The weekly group quota MUST also be present because retrieveUserQuotaSummary
// was served by the runtime host.
assert.ok(quotas.gemini_weekly, "weekly group quota present");
});
});

View File

@@ -21,7 +21,7 @@ test("isQuotaExhaustedForRequest isolates Claude and Gemini quota families for a
// Simulate Claude Opus being exhausted, while Gemini is NOT.
quotaCache.setQuotaCache(connectionId, "antigravity", {
"claude-opus-4-6-thinking": { remainingPercentage: 0, resetAt: null },
"gemini-3.5-flash-high": { remainingPercentage: 100, resetAt: null },
"gemini-3.7-flash-high": { remainingPercentage: 100, resetAt: null },
});
// Verify that Claude models are considered exhausted.
@@ -49,7 +49,7 @@ test("isQuotaExhaustedForRequest isolates Claude and Gemini quota families for a
quotaCache.isQuotaExhaustedForRequest(
connectionId,
"antigravity",
"antigravity/gemini-3.5-flash-high"
"antigravity/gemini-3.7-flash-high"
),
false,
"Gemini Flash should NOT be exhausted"
@@ -58,7 +58,7 @@ test("isQuotaExhaustedForRequest isolates Claude and Gemini quota families for a
quotaCache.isQuotaExhaustedForRequest(
connectionId,
"antigravity",
"antigravity/gemini-2.5-pro"
"antigravity/gemini-pro-agent"
),
false,
"Gemini Pro should share Gemini family quota and NOT be exhausted"
@@ -68,7 +68,7 @@ test("isQuotaExhaustedForRequest isolates Claude and Gemini quota families for a
const connectionIdAgy = "conn-agy-test";
quotaCache.setQuotaCache(connectionIdAgy, "agy", {
"claude-opus-4-6-thinking": { remainingPercentage: 0, resetAt: null },
"gemini-3.5-flash-high": { remainingPercentage: 100, resetAt: null },
"gemini-3.7-flash-high": { remainingPercentage: 100, resetAt: null },
});
assert.equal(
@@ -77,7 +77,7 @@ test("isQuotaExhaustedForRequest isolates Claude and Gemini quota families for a
"Claude Opus under 'agy' should be exhausted"
);
assert.equal(
quotaCache.isQuotaExhaustedForRequest(connectionIdAgy, "agy", "agy/gemini-3.5-flash-high"),
quotaCache.isQuotaExhaustedForRequest(connectionIdAgy, "agy", "agy/gemini-3.7-flash-high"),
false,
"Gemini Flash under 'agy' should NOT be exhausted"
);
@@ -112,8 +112,8 @@ test("isQuotaExhaustedForRequest isolates Claude and Gemini quota families for a
test("isQuotaExhaustedForRequest scopes gemini exhaustion to the requested model, not sibling models", () => {
const connectionId = "conn-gemini-sibling-test";
quotaCache.setQuotaCache(connectionId, "antigravity", {
"gemini-3.6-flash-medium": { remainingPercentage: 0, resetAt: null },
"gemini-2.5-pro": { remainingPercentage: 100, resetAt: null },
"gemini-3.7-flash-medium": { remainingPercentage: 0, resetAt: null },
"gemini-pro-agent": { remainingPercentage: 100, resetAt: null },
gemini_weekly: { remainingPercentage: 0, resetAt: null },
});
@@ -121,33 +121,33 @@ test("isQuotaExhaustedForRequest scopes gemini exhaustion to the requested model
quotaCache.isQuotaExhaustedForRequest(
connectionId,
"antigravity",
"antigravity/gemini-3.6-flash-medium"
"antigravity/gemini-3.7-flash-medium"
),
true,
"gemini-3.6 at 0% should be exhausted even when gemini-2.5-pro still has quota"
"gemini-3.7 at 0% should be exhausted even when gemini-pro-agent still has quota"
);
assert.equal(
quotaCache.isQuotaExhaustedForRequest(
connectionId,
"antigravity",
"antigravity/gemini-2.5-pro"
"antigravity/gemini-pro-agent"
),
false,
"gemini-2.5-pro should remain available when only gemini-3.6 is depleted"
"gemini-pro-agent should remain available when only gemini-3.7 Flash is depleted"
);
});
test("isQuotaExhaustedForRequest treats near-zero remaining as exhausted at default threshold", () => {
const connectionId = "conn-near-zero-test";
quotaCache.setQuotaCache(connectionId, "antigravity", {
"gemini-3.6-flash-medium": { remainingPercentage: 0.00000167, resetAt: null },
"gemini-3.7-flash-medium": { remainingPercentage: 0.00000167, resetAt: null },
});
assert.equal(
quotaCache.isQuotaExhaustedForRequest(
connectionId,
"antigravity",
"antigravity/gemini-3.6-flash-medium"
"antigravity/gemini-3.7-flash-medium"
),
true,
"effectively-zero remaining should count as exhausted"

View File

@@ -17,44 +17,40 @@ import { CLI_TOOLS } from "../../src/shared/constants/cliTools.ts";
const RETIRED_PUBLIC_MODELS = [
"gemini-3-pro-preview",
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-low",
"gemini-3.5-flash-extra-low",
"gemini-2.5-pro",
"gemini-2.5-flash-thinking",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-2.5-computer-use-preview-10-2025",
] as const;
const EXPECTED_LEADING_MODEL_ORDER = [
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"claude-opus-4-6-thinking",
"claude-sonnet-4-6",
"gemini-pro-agent",
"gemini-3.1-pro-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-low",
"gemini-3.5-flash-extra-low",
] as const;
const EXPECTED_ANTIGRAVITY_LEADING_MODEL_ORDER = [
"gemini-3.7-flash-high",
"gemini-3.7-flash-medium",
...EXPECTED_LEADING_MODEL_ORDER,
"gemini-3.7-flash-low",
"gemini-pro-agent",
"gemini-3.1-pro-low",
"gemini-3.1-flash-lite",
"claude-opus-4-6-thinking",
"claude-sonnet-4-6",
"gpt-oss-120b-medium",
] as const;
const ACTIVE_FLASH_MODEL_IDS = [
"gemini-3-flash-agent",
"gemini-3.5-flash-low",
"gemini-3.5-flash-extra-low",
] as const;
const CURRENT_36_FLASH_MODEL_IDS = [
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3.7-flash-high",
"gemini-3.7-flash-medium",
"gemini-3.7-flash-low",
] as const;
test("Antigravity and AGY place their live Gemini Flash tiers first", () => {
for (const [provider, models, expectedOrder] of [
["antigravity", ANTIGRAVITY_PUBLIC_MODELS, EXPECTED_ANTIGRAVITY_LEADING_MODEL_ORDER],
["antigravity", ANTIGRAVITY_PUBLIC_MODELS, EXPECTED_LEADING_MODEL_ORDER],
["agy", AGY_PUBLIC_MODELS, EXPECTED_LEADING_MODEL_ORDER],
] as const) {
assert.deepEqual(
@@ -114,7 +110,7 @@ test("Antigravity and AGY expose gemini-pro-agent and gemini-3.1-pro-high as cal
);
});
test("Antigravity support catalogs expose every live Gemini 3.6 Flash tier", () => {
test("Antigravity support catalogs expose every live Gemini 3.7 Flash tier", () => {
const antigravityModelIds = new Set(ANTIGRAVITY_PUBLIC_MODELS.map((model) => model.id));
const agyModelIds = new Set(AGY_PUBLIC_MODELS.map((model) => model.id));
const cliAliases = new Set(CLI_TOOLS.antigravity.modelAliases);
@@ -123,7 +119,7 @@ test("Antigravity support catalogs expose every live Gemini 3.6 Flash tier", ()
FREE_MODEL_BUDGETS.filter((model) => model.provider === "agy").map((model) => model.modelId)
);
for (const modelId of CURRENT_36_FLASH_MODEL_IDS) {
for (const modelId of ACTIVE_FLASH_MODEL_IDS) {
assert.equal(antigravityModelIds.has(modelId), true, `${modelId} missing from Antigravity`);
assert.equal(agyModelIds.has(modelId), true, `${modelId} missing from AGY`);
assert.equal(cliAliases.has(modelId), true, `${modelId} missing from CLI aliases`);
@@ -142,7 +138,7 @@ test("Antigravity support catalogs no longer advertise or price the rejected Hig
assert.ok(pricing["gemini-pro-agent"]);
});
test("Antigravity and AGY support metadata excludes the retired Gemini 3 Flash id", () => {
test("Antigravity and AGY support metadata excludes retired Flash ids", () => {
const cliAliases = CLI_TOOLS.antigravity.modelAliases;
const cliModelIds = CLI_TOOLS.antigravity.defaultModels.map((model) => model.id);
const agyFreeModelIds = FREE_MODEL_BUDGETS.filter((model) => model.provider === "agy").map(

View File

@@ -10,7 +10,7 @@ import { FORMATS } from "../../open-sse/translator/formats.ts";
test("antigravity reasoning capabilities: Gemini and Claude models support reasoning", () => {
const geminiModels = [
"antigravity/gemini-3-flash-agent",
"antigravity/gemini-3.7-flash-high",
"antigravity/gemini-pro-agent",
"antigravity/gemini-3.1-pro-low",
"antigravity/claude-sonnet-4-6",

View File

@@ -35,7 +35,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: undefined,
resetTime: "2026-05-26T00:00:00Z",
@@ -51,8 +51,8 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have quota for gemini-3-flash-agent");
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be 0%");
assert.equal(quota.unlimited, false, "should not be unlimited");
assert.equal(quota.used > 0, true, "used should be > 0 when quota is exhausted");
@@ -69,7 +69,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: 0,
resetTime: "2026-05-26T00:00:00Z",
@@ -88,8 +88,8 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have quota for gemini-3-flash-agent");
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be 0%");
assert.equal(quota.unlimited, false, "should not be unlimited");
}
@@ -105,7 +105,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: 1.0,
resetTime: "2026-05-26T00:00:00Z",
@@ -124,8 +124,8 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have quota for gemini-3-flash-agent");
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 100, "remaining should be 100%");
assert.equal(quota.unlimited, false, "should not be unlimited (has resetTime)");
}
@@ -176,7 +176,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: 0.5,
resetTime: "2026-05-26T00:00:00Z",
@@ -195,8 +195,8 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have quota for gemini-3-flash-agent");
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 50, "remaining should be 50%");
assert.equal(quota.unlimited, false, "should not be unlimited");
}
@@ -212,7 +212,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: 1.5,
resetTime: "2026-05-26T00:00:00Z",
@@ -231,8 +231,8 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have quota for gemini-3-flash-agent");
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 100, "remaining should be clamped to 100%");
assert.equal(quota.unlimited, false, "should not be unlimited (has resetTime)");
}
@@ -248,7 +248,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: {
remainingFraction: -0.5,
resetTime: "2026-05-26T00:00:00Z",
@@ -267,8 +267,8 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3-flash-agent"];
assert.ok(quota, "should have quota for gemini-3-flash-agent");
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be clamped to 0%");
assert.equal(quota.unlimited, false, "should not be unlimited");
}

View File

@@ -176,7 +176,7 @@ test("getUsageForProvider(antigravity) merges weekly quotas with the selected CL
json: async () => ({
buckets: [
{
modelId: "gemini-3-flash-agent",
modelId: "gemini-3.7-flash-high",
remainingFraction: 0.4,
resetTime: RESET_IN_2_HOURS,
},
@@ -190,7 +190,7 @@ test("getUsageForProvider(antigravity) merges weekly quotas with the selected CL
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: { remainingFraction: 1.0, resetTime: RESET_IN_2_HOURS },
},
},
@@ -211,8 +211,8 @@ test("getUsageForProvider(antigravity) merges weekly quotas with the selected CL
const quotas = (result as UsageResult).quotas;
// Existing per-model 5h quota is untouched.
assert.ok(quotas["gemini-3-flash-agent"], "per-model 5h quota still present");
assert.equal(quotas["gemini-3-flash-agent"].quotaSource, "retrieveUserQuota");
assert.ok(quotas["gemini-3.7-flash-high"], "per-model 5h quota still present");
assert.equal(quotas["gemini-3.7-flash-high"].quotaSource, "retrieveUserQuota");
// New weekly group quota is merged in alongside it.
assert.ok(quotas.gemini_weekly, "weekly group quota merged in");
@@ -235,7 +235,7 @@ test("getUsageForProvider(antigravity) is unaffected when retrieveUserQuotaSumma
ok: true,
json: async () => ({
models: {
"gemini-3-flash-agent": {
"gemini-3.7-flash-high": {
quotaInfo: { remainingFraction: 1.0, resetTime: RESET_IN_2_HOURS },
},
},
@@ -253,6 +253,6 @@ test("getUsageForProvider(antigravity) is unaffected when retrieveUserQuotaSumma
const result = await getUsageForProvider(connection, { forceRefresh: true });
const quotas = (result as UsageResult).quotas;
assert.ok(quotas["gemini-3-flash-agent"], "per-model quota still present without weekly data");
assert.ok(quotas["gemini-3.7-flash-high"], "per-model quota still present without weekly data");
assert.equal(quotas.gemini_weekly, undefined, "no weekly key when the RPC is unavailable");
});

View File

@@ -11,7 +11,7 @@
* `chat.ts` routes through `resolveAutoRoutingState` → `createVirtualAutoCombo`
* (autoRouting.ts), NOT through `createBuiltinAutoCombo`. So the chat path
* skipped the tier filter entirely and `auto/best-free` behaved as plain
* `auto/cheap`, allowing paid models (e.g. antigravity/gemini-3.6-flash-high)
* `auto/cheap`, allowing paid models (e.g. antigravity/gemini-3.7-flash-high)
* to be selected from the full pool.
*
* classifyAutoModel() is module-private, so this exercises it through the public

View File

@@ -87,9 +87,9 @@ test("credentialed providers expose one logical candidate per visible registry m
);
for (const model of [
"antigravity/claude-sonnet-4-6",
"antigravity/gemini-3.6-flash-low",
"antigravity/gemini-3.6-flash-medium",
"antigravity/gemini-3.6-flash-high",
"antigravity/gemini-3.7-flash-low",
"antigravity/gemini-3.7-flash-medium",
"antigravity/gemini-3.7-flash-high",
]) {
assert.ok(modelStrings.includes(model), `${model} should be eligible for auto routing`);
}
@@ -121,15 +121,15 @@ test("candidate transparency expands a logical model into per-account rows", asy
});
test("connection model exclusions narrow only that model's account allowlist", async () => {
const { first, second } = await seedConnections(["gemini-3.6-*"]);
const { first, second } = await seedConnections(["gemini-3.7-*"]);
const combo = await virtualFactory.createVirtualAutoCombo(undefined);
const candidates = antigravityCandidates(combo);
const geminiCandidates = candidates.filter((candidate) =>
candidate.model.startsWith("antigravity/gemini-3.6-")
candidate.model.startsWith("antigravity/gemini-3.7-")
);
assert.ok(geminiCandidates.length >= 3, "Gemini 3.6 candidates should remain available");
assert.ok(geminiCandidates.length >= 3, "Gemini 3.7 candidates should remain available");
for (const candidate of geminiCandidates) {
assert.deepEqual(candidate.allowedConnectionIds, [second.id]);
}
@@ -150,9 +150,9 @@ test("hiding the first registry model does not drop the credentialed provider",
assert.equal(modelStrings.includes("antigravity/claude-sonnet-4-6"), false);
for (const model of [
"antigravity/gemini-3.6-flash-low",
"antigravity/gemini-3.6-flash-medium",
"antigravity/gemini-3.6-flash-high",
"antigravity/gemini-3.7-flash-low",
"antigravity/gemini-3.7-flash-medium",
"antigravity/gemini-3.7-flash-high",
]) {
assert.ok(modelStrings.includes(model), `${model} should remain after Sonnet is hidden`);
}

View File

@@ -181,16 +181,10 @@ test("Antigravity Gemini-family regression covers every current callable tier",
for (const model of [
"gemini-pro-agent",
"gemini-3.1-pro-low",
"gemini-3.6-flash-high",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash-low",
"gemini-3.5-flash-extra-low",
"gemini-3.7-flash-high",
"gemini-3.7-flash-medium",
"gemini-3.7-flash-low",
"gemini-3.1-flash-lite",
"gemini-2.5-flash-thinking",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
]) {
assert.ok(coveredModels.has(model), `expected regression coverage for ${model}`);
}

View File

@@ -29,10 +29,9 @@ function mkPkg(
/**
* Build a root tree mirroring the real SLM optional shape:
* @atjsh/llmlingua-2 → dep es-toolkit, PEER @huggingface/transformers (+ tfjs, js-tiktoken)
* @tensorflow/tfjs → dep @tensorflow/tfjs-core → dep long
* @atjsh/llmlingua-2 → dep es-toolkit, PEER @huggingface/transformers (+ js-tiktoken)
* js-tiktoken → dep base64-js
* @huggingface/transformers present at root as a (stale) 4.2.0
* @huggingface/transformers present at root as a (hypothetical future) 5.0.0
*
* Each mock package gets a resolvable entrypoint so that isPackageIntact (which
* checks entrypoint integrity via require.resolve) can validate the co-located
@@ -49,26 +48,12 @@ function buildRoot(rootDir: string): void {
dependencies: { "es-toolkit": "^1.38.0" },
peerDependencies: {
"@huggingface/transformers": "*",
"@tensorflow/tfjs": "*",
"js-tiktoken": "*",
},
},
{ "dist/index.js": "export const llmlingua = true;\n" }
);
mkPkg(rootNm, "es-toolkit", { main: "index.js" }, { "index.js": "export const esToolkit = true;\n" });
mkPkg(
rootNm,
"@tensorflow/tfjs",
{ main: "index.js", dependencies: { "@tensorflow/tfjs-core": "4.22.0" } },
{ "index.js": "export const tfjs = true;\n" }
);
mkPkg(
rootNm,
"@tensorflow/tfjs-core",
{ main: "index.js", dependencies: { long: "^5.0.0" } },
{ "index.js": "export const tfjsCore = true;\n" }
);
mkPkg(rootNm, "long", { main: "index.js" }, { "index.js": "export const long = true;\n" });
mkPkg(
rootNm,
"js-tiktoken",
@@ -76,8 +61,8 @@ function buildRoot(rootDir: string): void {
{ "index.js": "export const tiktoken = true;\n" }
);
mkPkg(rootNm, "base64-js", { main: "index.js" }, { "index.js": "export const base64 = true;\n" });
// Root transformers is the STALE 4.x line — the bug we must not propagate into dist.
mkPkg(rootNm, "@huggingface/transformers", { version: "4.2.0" });
// Root transformers is a hypothetical FUTURE line — the version we must not propagate into dist.
mkPkg(rootNm, "@huggingface/transformers", { version: "5.0.0" });
}
test("computeDependencyClosure walks deps transitively and skips peers (transformers)", () => {
@@ -88,11 +73,8 @@ test("computeDependencyClosure walks deps transitively and skips peers (transfor
for (const expected of [
"@atjsh/llmlingua-2",
"@tensorflow/tfjs",
"js-tiktoken",
"es-toolkit",
"@tensorflow/tfjs-core",
"long",
"base64-js",
]) {
assert.ok(closure.includes(expected), `closure should include ${expected}`);
@@ -111,23 +93,20 @@ test("colocateLlmlinguaOptionals copies the closure into dist and never clobbers
const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-copy-"));
try {
buildRoot(root);
// dist already ships the PINNED transformers (3.5.2) — must survive untouched.
// dist already ships the PINNED transformers (4.2.0) — must survive untouched.
const distNm = join(root, "dist", "node_modules");
mkPkg(distNm, "@huggingface/transformers", { version: "3.5.2" });
mkPkg(distNm, "@huggingface/transformers", { version: "4.2.0" });
const result = colocateLlmlinguaOptionals({ rootDir: root });
assert.equal(result.skipped, false);
if (result.skipped === false) {
assert.ok(result.copied >= 6, `expected >=6 packages copied, got ${result.copied}`);
assert.ok(result.copied >= 4, `expected >=4 packages copied, got ${result.copied}`);
}
// Full closure landed in dist/node_modules.
for (const name of [
"@atjsh/llmlingua-2",
"es-toolkit",
"@tensorflow/tfjs",
"@tensorflow/tfjs-core",
"long",
"js-tiktoken",
"base64-js",
]) {
@@ -136,11 +115,11 @@ test("colocateLlmlinguaOptionals copies the closure into dist and never clobbers
// The package payload came along (not just the manifest).
assert.ok(existsSync(join(distNm, "@atjsh", "llmlingua-2", "dist", "index.js")));
// CRITICAL: dist's pinned transformers is preserved — root's 4.2.0 must NOT win.
// CRITICAL: dist's pinned transformers is preserved — root's 5.0.0 must NOT win.
const distTransformers = JSON.parse(
readFileSync(join(distNm, "@huggingface", "transformers", "package.json"), "utf8")
);
assert.equal(distTransformers.version, "3.5.2", "dist transformers must remain 3.5.2");
assert.equal(distTransformers.version, "4.2.0", "dist transformers must remain 4.2.0");
} finally {
rmSync(root, { recursive: true, force: true });
}
@@ -150,7 +129,7 @@ test("colocateLlmlinguaOptionals is idempotent (second run is a no-op)", () => {
const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-idem-"));
try {
buildRoot(root);
mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "3.5.2" });
mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "4.2.0" });
const first = colocateLlmlinguaOptionals({ rootDir: root });
assert.equal(first.skipped, false);
@@ -169,7 +148,7 @@ test("colocateLlmlinguaOptionals skips when SLM optionals are not installed", ()
const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-noopt-"));
try {
// dist bundle exists, but the optional seeds were never installed at root.
mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "3.5.2" });
mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "4.2.0" });
mkdirSync(join(root, "node_modules"), { recursive: true });
const result = colocateLlmlinguaOptionals({ rootDir: root });
@@ -208,7 +187,7 @@ test("colocateLlmlinguaOptionals fills a Next-traced stub (package.json only, no
try {
buildRoot(root);
const distNm = join(root, "dist", "node_modules");
mkPkg(distNm, "@huggingface/transformers", { version: "3.5.2" });
mkPkg(distNm, "@huggingface/transformers", { version: "4.2.0" });
// Simulate the Next-traced stub: directory exists, package.json only.
const stubDir = join(distNm, "@atjsh", "llmlingua-2");
@@ -233,5 +212,5 @@ test("colocateLlmlinguaOptionals fills a Next-traced stub (package.json only, no
test("SEED_PACKAGES excludes transformers (it is a dist-pinned peer, not a seed)", () => {
assert.ok(!SEED_PACKAGES.includes("@huggingface/transformers"));
assert.deepEqual(SEED_PACKAGES, ["@atjsh/llmlingua-2", "@tensorflow/tfjs", "js-tiktoken"]);
assert.deepEqual(SEED_PACKAGES, ["@atjsh/llmlingua-2", "js-tiktoken"]);
});

View File

@@ -210,7 +210,7 @@ test("priority combo: quota 429 on passthrough provider does not skip another mo
const combo = await combosDb.createCombo({
name: "passthrough-quota-scope",
strategy: "priority",
models: ["antigravity/claude-opus-4-6-thinking", "antigravity/gemini-3-flash-agent"],
models: ["antigravity/claude-opus-4-6-thinking", "antigravity/gemini-3.7-flash-high"],
});
const response = await handleComboChat({
@@ -235,7 +235,7 @@ test("priority combo: quota 429 on passthrough provider does not skip another mo
});
assert.equal(response.status, 200);
assert.equal(calls.at(-1), "antigravity/gemini-3-flash-agent");
assert.equal(calls.at(-1), "antigravity/gemini-3.7-flash-high");
assert.ok(
calls.includes("antigravity/claude-opus-4-6-thinking"),
"first passthrough model should be attempted before fallback"

View File

@@ -0,0 +1,72 @@
/**
* Gate de estágio × engine sem `metadata`.
*
* `assertValidEngine()` (engines/registry.ts) valida `id`, `apply`, `compress`,
* `getConfigSchema` e `validateConfig` — NÃO exige `metadata`. Uma engine sem
* esse campo é, portanto, um registro legal. Mas `canRunAtCompressionStage`
* lia `engine.metadata.executionStages` sem guarda, então essa engine legal
* derrubava o pipeline inteiro com `TypeError: Cannot read properties of
* undefined` — em vez de falhar aberto, que é o contrato da compressão.
*
* O fallback documentado para quem não declara `executionStages` é "só
* pre-translation". Metadata ausente é o mesmo caso de "não declarou", e tem de
* cair no mesmo fallback.
*/
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import {
registerCompressionEngine,
unregisterCompressionEngine,
} from "../../../open-sse/services/compression/engines/registry.ts";
import { applyStackedCompression } from "../../../open-sse/services/compression/strategySelector.ts";
import type { CompressionEngine } from "../../../open-sse/services/compression/engines/types.ts";
const ENGINE_ID = "metadata-less-test-engine";
const body = () => ({ messages: [{ role: "user", content: "hello world" }] });
describe("stage gate — engine sem metadata", () => {
beforeEach(() => {
// Registro deliberadamente sem `metadata`: é o que assertValidEngine aceita.
registerCompressionEngine({
id: ENGINE_ID,
name: "metadata-less test engine",
targets: ["messages"],
stackable: true,
apply(input: Record<string, unknown>) {
return { body: input, compressed: false, stats: null };
},
compress() {
return { text: "", stats: null };
},
getConfigSchema() {
return [];
},
validateConfig() {
return { valid: true, errors: [] };
},
} as unknown as CompressionEngine);
});
afterEach(() => unregisterCompressionEngine(ENGINE_ID));
it("não derruba o pipeline no estágio pré-tradução (default)", () => {
const r = applyStackedCompression(body(), [{ engine: ENGINE_ID }]);
assert.equal(r.compressed, false);
assert.deepEqual(r.body, body());
});
it("não derruba o pipeline no estágio pós-tradução — pula pelo fallback", () => {
const r = applyStackedCompression(body(), [{ engine: ENGINE_ID }], {
compressionStage: "post-translation",
});
assert.equal(r.compressed, false);
assert.deepEqual(r.body, body());
// Fallback documentado: quem não declara estágio só roda pre-translation.
assert.ok(
r.stats?.validationWarnings?.some((w) => w.includes(ENGINE_ID) && w.includes("skipped")),
`esperado aviso de skip por estágio, veio ${JSON.stringify(r.stats?.validationWarnings)}`
);
});
});

View File

@@ -1,7 +1,7 @@
import { test } from "node:test";
import assert from "node:assert";
import { estimateCompressionTokens } from "../../../open-sse/services/compression/stats.ts";
import { transformAnthropicMessages } from "omniglyph";
import { transformAnthropicMessages, transformOpenAIChatCompletions } from "omniglyph";
const CHARS_PER_TOKEN = 4;
@@ -67,3 +67,33 @@ test("regressão: corpo sem imagem estima o MESMO valor de antes (char-count pur
const expected = Math.ceil(JSON.stringify(plainBody).length / CHARS_PER_TOKEN);
assert.equal(estimateCompressionTokens(plainBody), expected);
});
test("estimateCompressionTokens contabiliza image_url PNG no wire OpenAI", async () => {
const originalBody = {
model: "gpt-5.6",
messages: [
{ role: "system", content: DENSE },
{ role: "user", content: "oi" },
],
};
const encoded = new TextEncoder().encode(JSON.stringify(originalBody));
const result = await transformOpenAIChatCompletions(encoded);
assert.equal(result.info.compressed, true);
const compressedBody = JSON.parse(new TextDecoder().decode(result.body)) as Record<
string,
unknown
>;
assert.ok(JSON.stringify(compressedBody).includes('"type":"image_url"'));
const originalEstimate = estimateCompressionTokens(originalBody);
const compressedEstimate = estimateCompressionTokens(compressedBody);
const naiveEstimate = Math.ceil(JSON.stringify(compressedBody).length / CHARS_PER_TOKEN);
assert.ok(
compressedEstimate < naiveEstimate,
`expected image-aware (${compressedEstimate}) < base64 char estimate (${naiveEstimate})`
);
assert.ok(
compressedEstimate < originalEstimate,
`expected OpenAI image compression (${compressedEstimate}) < original (${originalEstimate})`
);
});

View File

@@ -1,8 +1,8 @@
/**
* Tests for the real LLMLingua worker-thread backend (`worker.ts` + `onnxWorker.ts`).
*
* The four optional deps (`@atjsh/llmlingua-2`, `@huggingface/transformers`,
* `@tensorflow/tfjs`, `js-tiktoken`) are NOT installed in this worktree, so the
* The three optional deps (`@atjsh/llmlingua-2`, `@huggingface/transformers`,
* `js-tiktoken`) are NOT installed in this worktree, so the
* default path MUST fail-open WITHOUT spawning a worker:
*
* 1. Deps absent → fail-open, no spawn (ALWAYS runs here): the backend returns the
@@ -23,12 +23,11 @@ import {
const require = createRequire(import.meta.url);
/** Whether all four optional deps resolve in this environment. */
/** Whether all three optional deps resolve in this environment. */
function depsResolve(): boolean {
try {
require.resolve("@atjsh/llmlingua-2");
require.resolve("@huggingface/transformers");
require.resolve("@tensorflow/tfjs");
require.resolve("js-tiktoken");
return true;
} catch {

View File

@@ -18,11 +18,17 @@ function claudeBody(): Record<string, unknown> {
messages: [{ role: "user", content: [{ type: "text", text: "oi" }] }],
};
}
const OK = { model: "claude-fable-5", supportsVision: true, providerTransport: "direct" as const };
const OK = {
model: "claude-fable-5",
supportsVision: true,
providerTransport: "direct" as const,
imageTransportFidelity: "byte-preserving" as const,
};
const GPT_OK = {
model: "gpt-5.6",
supportsVision: true,
providerTransport: "direct" as const,
imageTransportFidelity: "byte-preserving" as const,
sourceFormat: "openai" as const,
targetFormat: "openai" as const,
compressionStage: "post-translation" as const,
@@ -82,6 +88,15 @@ test("skip fail-closed: sem supportsVision / transporte agregador / undefined",
}
});
test("skip fail-closed: rota direta sem recibo de fidelidade de imagem", async () => {
const r = await omniglyphEngine.applyAsync!(claudeBody(), {
...OK,
imageTransportFidelity: "unknown",
});
assert.equal(r.compressed, false);
assert.ok(r.stats?.techniquesUsed.includes("skip:transport_fidelity_unknown"));
});
test("skip: modelo fora da allowlist medida", async () => {
const body = { ...claudeBody(), model: "gpt-5.5" };
const r = await omniglyphEngine.applyAsync!(body, { ...OK, model: "gpt-5.5" });
@@ -110,6 +125,31 @@ test("cache_control do cliente sobrevive byte a byte", async () => {
assert.ok(JSON.stringify(r.body).includes('"cache_control"'));
});
test("preserveSystemPrompt mantém o sistema nativo e ainda pode comprimir tool_result", async () => {
const body = {
...claudeBody(),
messages: [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "call_1",
content: DENSE,
},
],
},
],
};
const r = await omniglyphEngine.applyAsync!(body, {
...OK,
config: { preserveSystemPrompt: true } as never,
});
assert.equal(r.compressed, true);
assert.equal((r.body as { system?: unknown }).system, DENSE);
assert.ok(JSON.stringify(r.body).includes('"type":"image"'));
});
test("apply síncrono é pass-through seguro (engine async-only)", () => {
const body = claudeBody();
const r = omniglyphEngine.apply(body, OK);
@@ -147,3 +187,139 @@ test("OpenAI não roda no estágio pré-tradução", async () => {
assert.equal(r.compressed, false);
assert.ok(r.stats?.techniquesUsed.includes("skip:requires_post_translation"));
});
// OmniGlyph 1.4.0 introduziu escopos de segurança (`coding-safe`/`balanced`/
// `aggressive`/`passthrough`) e passou a resolvê-los, dentro de
// `isOmniGlyphSupportedModel()`, lendo `process.env.OMNIGLYPH_PROFILE`. Isso
// transforma uma variável de ambiente do HOST num gate silencioso de TODO
// request do OmniRoute: um `OMNIGLYPH_PROFILE=passthrough` exportado no shell
// do processo desligaria a engine sem que nenhuma configuração do OmniRoute
// tivesse mudado — e sem nenhum sinal na UI. A política é do OmniRoute; o
// adapter tem de passar o escopo explicitamente.
async function withEnv<T>(key: string, value: string, fn: () => Promise<T>): Promise<T> {
const had = Object.prototype.hasOwnProperty.call(process.env, key);
const previous = process.env[key];
process.env[key] = value;
try {
return await fn();
} finally {
if (had) process.env[key] = previous;
else delete process.env[key];
}
}
test("OMNIGLYPH_PROFILE do host não decide o gate de modelo do OmniRoute", async () => {
const r = await withEnv("OMNIGLYPH_PROFILE", "passthrough", () =>
omniglyphEngine.applyAsync!(claudeBody(), OK)
);
assert.equal(
r.compressed,
true,
"env do processo não pode desligar a engine: o escopo vem da config do OmniRoute"
);
});
test("OMNIGLYPH_PROFILE do host não amplia a allowlist de modelos do OmniRoute", async () => {
const body = { ...claudeBody(), model: "claude-sonnet-5" };
const r = await withEnv("OMNIGLYPH_PROFILE", "aggressive", () =>
withEnv("OMNIGLYPH_MODELS", "claude-fable-5,claude-sonnet-5", () =>
omniglyphEngine.applyAsync!(body, { ...OK, model: "claude-sonnet-5" })
)
);
assert.equal(r.compressed, false, "modelo sem recibo medido não pode entrar via env do host");
assert.ok(r.stats?.techniquesUsed.includes("skip:model_not_approved"));
});
// ---------------------------------------------------------------------------
// Perfis semânticos (OmniGlyph 1.4.0)
//
// `transformAnthropicMessages()` resolve e mescla o perfil sozinho, mas os
// transformadores OpenAI recebem `TransformOptions` cru e NÃO conhecem
// `profile`. Sem o merge explícito do host, um perfil escolhido pelo operador
// valeria no wire Claude e seria silenciosamente ignorado no wire OpenAI.
// ---------------------------------------------------------------------------
/** Histórico longo o bastante para o colapso valer mesmo com o system nativo. */
function claudeBodyWithHistory(): Record<string, unknown> {
const messages: Array<Record<string, unknown>> = [];
for (let i = 0; i < 40; i++) {
messages.push({ role: "user", content: [{ type: "text", text: `pergunta ${i}: ${DENSE}` }] });
messages.push({
role: "assistant",
content: [{ type: "text", text: `resposta ${i}: ${DENSE}` }],
});
}
messages.push({ role: "user", content: [{ type: "text", text: "oi" }] });
return { model: "claude-fable-5", max_tokens: 128, system: DENSE, messages };
}
test("perfil coding-safe chega ao wire OpenAI (não só ao wrapper Anthropic)", async () => {
const aggressive = await omniglyphEngine.applyAsync!(openaiChatBody(), {
...GPT_OK,
stepConfig: { profile: "aggressive" } as never,
});
assert.equal(aggressive.compressed, true, "aggressive é a política medida atual");
const codingSafe = await omniglyphEngine.applyAsync!(openaiChatBody(), {
...GPT_OK,
stepConfig: { profile: "coding-safe" } as never,
});
assert.equal(
codingSafe.compressed,
false,
"coding-safe mantém system e schemas de tools nativos no wire OpenAI"
);
assert.ok(
codingSafe.stats?.techniquesUsed.some((t) => t.startsWith("skip:below_min_chars")),
`esperado skip por minCompressChars, veio ${JSON.stringify(codingSafe.stats?.techniquesUsed)}`
);
});
test("perfil é TETO, não piso: coding-safe mantém o system nativo mesmo sem preserveSystemPrompt", async () => {
const body = claudeBodyWithHistory();
const r = await omniglyphEngine.applyAsync!(body, {
...OK,
stepConfig: { profile: "coding-safe", preserveSystemPrompt: false } as never,
});
assert.equal(r.compressed, true, "o histórico antigo ainda colapsa");
assert.equal(
(r.body as { system?: unknown }).system,
DENSE,
"coding-safe não deixa um override do chamador reabrir a compressão do system"
);
});
test("perfil passthrough desliga a engine sem tocar no corpo", async () => {
const body = claudeBodyWithHistory();
const r = await omniglyphEngine.applyAsync!(body, {
...OK,
stepConfig: { profile: "passthrough" } as never,
});
assert.equal(r.compressed, false);
assert.ok(r.stats?.techniquesUsed.includes("skip:profile_passthrough"));
assert.deepEqual(r.body, body);
});
test("perfil inválido falha fechado, não propaga exceção", async () => {
const r = await omniglyphEngine.applyAsync!(claudeBody(), {
...OK,
stepConfig: { profile: "turbo-max" } as never,
});
assert.equal(r.compressed, false);
assert.ok(r.stats?.techniquesUsed.includes("skip:invalid_profile"));
});
test("preserveSystemPrompt: wire OpenAI pula, porque o pacote não sabe preservar system lá", async () => {
// O transform OpenAI do OmniGlyph 1.4.0 honra apenas compressTools, gptHistory,
// minCompressChars e reflow — não existe compressSystem nesse wire, e a
// instrução vira sempre um ponteiro para a imagem. Imagear assim queimaria o
// prefixo quente que a decisão cache-aware do OmniRoute mandou preservar, e o
// OmniRoute não teria como saber. Sem opção de honrar a política, pula.
const r = await omniglyphEngine.applyAsync!(openaiChatBody(), {
...GPT_OK,
config: { preserveSystemPrompt: true } as never,
});
assert.equal(r.compressed, false);
assert.ok(r.stats?.techniquesUsed.includes("skip:system_preservation_unsupported_on_wire"));
assert.deepEqual(r.body, openaiChatBody());
});

View File

@@ -1,12 +1,27 @@
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";
import { test } from "node:test";
import { resolveOmniGlyphTransport } from "../../../open-sse/services/compression/imageTransportPolicy.ts";
test("chatCore treats both Anthropic providers as direct OmniGlyph transports", () => {
test("chatCore uses the measured Anthropic byte-preserving transport policy", () => {
const chatCore = readFileSync("open-sse/handlers/chatCore.ts", "utf8");
assert.match(
chatCore,
/providerTransport:\s*provider === "anthropic"\s*\|\|\s*provider === "claude"[\s\S]{0,160}?"direct"/
);
assert.match(chatCore, /resolveOmniGlyphTransport\(provider\)/);
const translationIndex = chatCore.indexOf("translatedBody = translateRequest(");
const postCompressionIndex = chatCore.indexOf("if (runPostTranslationCompression");
assert.ok(translationIndex >= 0, "chatCore deve manter a tradução explícita");
assert.ok(postCompressionIndex > translationIndex, "OmniGlyph target-wire roda após tradução");
assert.match(chatCore, /compressionStage: "post-translation"/);
assert.deepEqual(resolveOmniGlyphTransport("anthropic"), {
providerTransport: "direct",
imageTransportFidelity: "byte-preserving",
});
assert.deepEqual(resolveOmniGlyphTransport("claude"), {
providerTransport: "direct",
imageTransportFidelity: "byte-preserving",
});
assert.deepEqual(resolveOmniGlyphTransport("openai"), {
providerTransport: "aggregator",
imageTransportFidelity: "unknown",
});
});

View File

@@ -6,6 +6,7 @@ test("pacote omniglyph exporta a API que o adapter consome", async () => {
assert.equal(typeof mod.transformAnthropicMessages, "function");
assert.equal(typeof mod.transformOpenAIChatCompletions, "function");
assert.equal(typeof mod.transformOpenAIResponses, "function");
assert.equal(typeof mod.transformRequest, "function");
assert.equal(typeof mod.isOmniGlyphSupportedModel, "function");
assert.equal(typeof mod.isOmniGlyphSupportedGptModel, "function");
assert.equal(mod.isOmniGlyphSupportedModel("claude-fable-5"), true);
@@ -14,3 +15,30 @@ test("pacote omniglyph exporta a API que o adapter consome", async () => {
const applicability = await import("omniglyph/applicability");
assert.equal(typeof applicability.isModelImageable, "function");
});
// Superfícies introduzidas no 1.4.0. Sem esta asserção, uma remoção upstream só
// apareceria em runtime — o adapter importa esses símbolos diretamente.
test("omniglyph 1.4.0 exporta escopo de segurança, perfis e accounting", async () => {
const mod = await import("omniglyph");
// Gate de modelo por escopo explícito (não pela env do processo).
assert.equal(typeof mod.isOmniGlyphSupportedModelForScope, "function");
assert.equal(mod.isOmniGlyphSupportedModelForScope("claude-fable-5", "coding-safe"), true);
assert.equal(mod.isOmniGlyphSupportedModelForScope("claude-sonnet-5", "coding-safe"), false);
assert.equal(
mod.isOmniGlyphSupportedModelForScope("claude-fable-5", "passthrough"),
false,
"passthrough não habilita modelo nenhum"
);
// Perfis semânticos.
assert.equal(typeof mod.resolveCompressionProfile, "function");
assert.equal(typeof mod.mergeCompressionProfileOptions, "function");
assert.equal(typeof mod.shouldKeepToolResultSharp, "function");
assert.equal(mod.resolveCompressionProfile("coding-safe").name, "coding-safe");
assert.throws(() => mod.resolveCompressionProfile("nao-existe"));
// Contabilidade física normalizada.
assert.equal(typeof mod.normalizeAccounting, "function");
assert.equal(typeof mod.providerActualInputTokens, "function");
});

Some files were not shown because too many files have changed in this diff Show More