feat(dashboard): continuous call-log export to pluggable destinations (BigQuery first) - #11945
Conversation
Ships the call logs behind the Logs tab to an external analytics store on an hourly schedule. A destination is a plugin: it declares a Zod config schema, a UI field descriptor, which keys are secret, and a client that can test, prepare and send. Adding Datadog or Grafana Loki is one file plus one registry line, so the runner, the REST layer and the dashboard form stay destination-agnostic. Google BigQuery is the first destination. It talks the REST API directly with a self-signed RS256 assertion rather than pulling in a Google SDK, chunks a batch into insertAll calls of at most 500 rows, retries transient statuses with backoff, and keys every row by the call-log id so a retried chunk is de-duplicated instead of double-written. A partial failure arrives as HTTP 200 with insertErrors, which is treated as a failure so the cursor cannot advance past rows BigQuery never accepted. The cursor is call_logs.rowid, not timestamp: saveCallLog accepts a caller-supplied timestamp, so a slow request can be written after a faster one that started later and a timestamp cursor would skip it. A cursor above MAX(rowid) means the table was purged and rowids restarted, so the runner rewinds rather than going permanently blind. Service-account keys are encrypted at rest and never returned by any endpoint. Because encrypt() is a silent passthrough without STORAGE_ENCRYPTION_KEY, storing a destination that carries a credential is refused when the key is absent instead of writing it to SQLite in plaintext.
Found while exporting into a real BigQuery dataset end to end. A table created moments earlier is not yet visible to the streaming endpoint, which answers 404 for a few seconds. That 404 is now retried, but only when the run itself created the table, so a genuinely missing table still fails fast. The retry's sleep used an unref'd timer, which let the process exit out from under an in-flight retry when nothing else held the event loop open. Also adds the extensibility proof the plugin contract was missing: a fake destination registered through the registry drives the runner, the secret handling and the presenter with no BigQuery code loaded, and a companion test asserts no destination-specific vocabulary has leaked into the generic layers. That test caught two real leaks: describeLogExportDestinationTypes() read the static array instead of the live registry, so a registered destination was invisible to the dashboard, and the config form carried a BigQuery-specific placeholder. Migration 170 takes the repo to 167 migrations; the three docs that hardcode that count and the 42 llm.txt mirrors are updated to match.
Bring inherited Fast Quality Gates / ESLint / unit-test / docs-sync fixes from diegosouzapw#11940/diegosouzapw#11955/diegosouzapw#11975. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
… db module The db-rules gate requires every src/lib/db module to be re-exported from src/lib/localDb.ts, and the locale parity tests require every en.json key to exist in all 42 locales. - localDb.ts re-exports ./db/logExportDestinations - sidebar.logExport / sidebar.logExportSubtitle translated into all 42 locales
…uires Every page under docs/ is compiled by fumadocs-mdx, whose schema requires a title. Without it the production build fails with "invalid frontmatter ... title: expected string, received undefined". Turbopack hides this: it panics inside ImportTracer::get_traces while formatting the import trace for the error, so the build dies with "internal error: entered unreachable code" and never prints the cause. Building with OMNIROUTE_USE_TURBOPACK=0 shows the real message.
|
All gates are green except That job is fork-PR-only and I did not want to leave the production build unverified on that basis, so I built and ran it locally instead: The image builds, boots, and serves. Against it: a real Building with |
…t createTask flow (diegosouzapw#11296) (diegosouzapw#11985) flux/kontext is catalogued with isMarket: true, so handleKieImageGeneration routed it through KIE's unified Market createTask endpoint with model: "flux/kontext". KIE does not expose Flux Kontext through the Market catalog at all -- it lives under a dedicated API tree (POST /api/v1/flux/kontext/generate, poll GET /api/v1/flux/kontext/record-info, models flux-kontext-pro/flux-kontext-max) -- so the Market endpoint rejected it with "model name not supported", matching the reporter's exact error text. Special-case flux/kontext ahead of the isMarket branch so it hits the dedicated endpoint/payload shape instead of being treated as a Market entry. z-image/4.0-*/4.5-* remains intentionally untouched (still blocked on reporter/live confirmation per the existing in-code comment). Co-authored-by: Markus Hartung <mail@hartmark.se>
…outs (diegosouzapw#11500) (diegosouzapw#11989) * fix(db): rate-limit Arena ELO fetch-failure warnings on repeated timeouts (diegosouzapw#11500) * test(quality): split the diegosouzapw#11500 fetch-failure-dedup tests into their own file tests/unit/arena-elo-sync.test.ts crossed the 1000-line new-test-file cap (file-size gate, PR mode). The two new tests don't need the file's DB fixture (fetchArenaLeaderboards() never touches the DB), so they move to a self-contained sibling file instead of growing the frozen suite. --------- Co-authored-by: Markus Hartung <mail@hartmark.se>
…iegosouzapw#11600) (diegosouzapw#11983) Co-authored-by: Markus Hartung <mail@hartmark.se>
…te path (diegosouzapw#11707) (diegosouzapw#11984) enforceCodexResponsesLiteParallelToolCalls() forces parallel_tool_calls:false at the top of CodexExecutor.execute(), but transformRequest() early-returns the body before its RESPONSES_API_ALLOWLIST field filter only when _nativeCodexPassthrough is set. Any request that reaches the codex executor via the translated (non-native-passthrough) path never gets that flag, so the allowlist filter silently deleted parallel_tool_calls right before the fetch body was sent, reproducing the reported upstream rejection ('X-OpenAI-Internal-Codex-Responses-Lite requires parallel_tool_calls to be false') for every model. Add parallel_tool_calls to RESPONSES_API_ALLOWLIST so the value survives the translated path too. Update the sibling diegosouzapw#2608 allowlist test that previously asserted parallel_tool_calls gets stripped like other Chat Completions-only fields -- it is a legitimate Responses API field that must now survive. Co-authored-by: Markus Hartung <mail@hartmark.se>
…publish's opencode-plugin skip check (diegosouzapw#11787) (diegosouzapw#11990) * fix(cli): drop the never-produced dist/index.cjs requirement from prepublish's opencode-plugin skip check (diegosouzapw#11787) * test(build): resolve tsup/npm portably in the diegosouzapw#11787 regression test instead of a hardcoded .bin path The old test assumed @omniroute/opencode-plugin/node_modules/.bin/tsup already existed. A fresh checkout (CI's npm ci never installs this standalone package's own deps) has no such node_modules at all, so the test failed with MODULE_NOT_FOUND in CI while passing locally on a devbox that had installed it before. Mirror scripts/build/prepublish.ts's own install-then-resolveLocalBinEntry approach. --------- Co-authored-by: Markus Hartung <mail@hartmark.se>
…dispatches (diegosouzapw#11810) (diegosouzapw#11986) Co-authored-by: Markus Hartung <mail@hartmark.se>
…ic alias (diegosouzapw#11824) (diegosouzapw#11988) Co-authored-by: Markus Hartung <mail@hartmark.se>
…ked rows (diegosouzapw#9133) (diegosouzapw#11994) * fix(sse): stop the auto-combo candidates inspector from dropping blocked rows (diegosouzapw#9133) prepareVirtualAutoComboInputs applied filterResilienceBlockedCandidates before the diegosouzapw#7819 read-only candidate inspector ever saw the pool, so a model-locked or cooled-down candidate silently disappeared from /auto-combo/*/candidates instead of showing up as reachable:false with a reason (modelLocked/connectionCooldown/breakerState were dead fields by construction). Add an opt-in `skip` parameter so the inspector builds its own unfiltered pool; routing (createVirtualAutoCombo/createBuiltinAutoCombo called without a prepared override) is unchanged. Also aligns isModelLocked's model argument to the bare model id, matching every lock writer and the routing-side filter, instead of the "provider/model" string. Regression test: tests/unit/auto-combo-candidates-locked-model-visible.test.ts (red before the fix — locked account's row silently missing; green after). * chore(quality): register the diegosouzapw#9133 regression test in stryker tap.testFiles tests/unit/auto-combo-candidates-locked-model-visible.test.ts covers open-sse/services/accountFallback.ts (via isModelLocked) but wasn't listed, so its mutant kills wouldn't count toward mutation coverage. --------- Co-authored-by: Markus Hartung <mail@hartmark.se>
…SBOM on dispatch (twin of diegosouzapw#11982 + diegosouzapw#12020) (diegosouzapw#12022) * fix(release): resync the electron lockfile, build a dispatch from a repaired ref, keep curated notes, attach the SBOM on dispatch (release/v3.8.51 twin of diegosouzapw#11982 + diegosouzapw#12020) Same four changes as diegosouzapw#11982 and diegosouzapw#12020 on main, applied to this branch's own copies: - electron/package-lock.json regenerated (271 -> 284 entries): the optional electron-builder-squirrel-windows subtree was missing and `npm ci` refused the lock (EUSAGE) on the Linux and macOS legs; a clean `npm ci --ignore-scripts` on the result exits 0. - electron-release.yml: `build_ref` dispatch input (default: the version tag) and `generate_release_notes` only on the tag push (a re-attach dispatch appended GitHub's auto notes to the curated body on v3.8.50). - npm-publish.yml: the SBOM attaches to the GitHub Release on workflow_dispatch publishes too, whenever a release for the tag exists. actionlint and prettier clean; electron-release-desktop-channel-8949, electron-release-efficiency, electron-release-latest-yml.repro, check-workflows and npm-publish-artifact-provenance suites pass. * fix(release): validate build_ref in the validate job before any checkout uses it CodeQL (actions/cache-poisoning/poisonable-step, high) on release/v3.8.51 — the default branch: a raw dispatch input checked out next to setup-node's npm cache is a cache-poisoning vector. The input now goes through the validate job's regex allowlist (main or release/vX.Y.Z, empty = the version tag) and every build job checks out needs.validate.outputs.build_ref, never the input itself. * fix(release): drop the build_ref input — a dispatch builds the ref it is dispatched on CodeQL (actions/cache-poisoning/poisonable-step) tracks the input through the validate job's output regardless of the regex allowlist: an input-controlled checkout next to setup-node's npm cache on the default branch is a cache-poisoning vector. The ref is not an input any more; the checkouts use github.ref, so `gh workflow run electron-release.yml --ref v3.8.50 -f version=v3.8.50` rebuilds the tag and `--ref main` builds the repaired line. The tag-push path is unchanged.
…iegosouzapw#12021) * fix(ci): stop hosted docker-publish OOM and unpaint Build (advisory) docker-publish was firing 8 concurrent hosted builds on every merge storm; each died ResourceExhausted in npm run build (diegosouzapw#11976). One publish per ref, webpack instead of Turbopack so native RSS stays inside the V8 heap we can cap. Build (advisory) is skipped: continue-on-error still reports FAILURE and was painting every fork PR red. Closes diegosouzapw#11976 * fix(ci): run docker-publish amd64 on omni-build and share the heavy lane The .113 box is 31 GB / 32 cores — enough for one next-build. Hosted ubuntu-24.04 is ~7 GB and ResourceExhausted every publish (diegosouzapw#11976). amd64 now targets [self-hosted, omni-build] (Turbopack) when USE_VPS_RUNNER is on, joins the existing heavy-build-main group so it queues beside ci.yml Build instead of becoming a third heavy, and falls back to hosted + webpack if the VPS is off. arm64 stays on ubuntu-24.04-arm with webpack (no ARM box). * test(ci): align the advisory-build contract with the hosted-OOM skip if: ${{ false }} tripped zizmor obfuscation (194→195). Bare if: false skips the job without a new finding. The diegosouzapw#7307 test now pins the skip and keeps the job body as the restore recipe.
…out for querying Two additions to the log export. Payloads. The export carried only the summary fields the Logs list shows. A destination can now opt into `includeBodies`, which additionally ships what the Logs detail pane shows: the request and response bodies plus the pipeline's route decision, client raw request, normalised OpenAI request, provider-dialect request, provider response and client response. This is prompt content, so it is off by default and set per destination. What ships is what the dashboard shows, because both read through `getCallLogById`: payloads are already PII-sanitised and secret-redacted when written, and a call made with a `noLog` API key stores none, so there is nothing to export. Rows whose artifact is missing export their summary rather than failing the batch. `maxBodyBytes` caps each field and truncates rather than dropping, flagging the row with `bodies_truncated`. insertAll chunking now closes on bytes as well as row count. 500 rows of summaries is small; 500 rows carrying prompts can be tens of megabytes against a 10 MB request cap. Table layout. The auto-created table was day-partitioned but unclustered. It now clusters on api_key_name, provider, model, status, ordered by how call logs are actually filtered, and accepts an optional partition retention.
Live run: dashboard and BigQuery evidenceEverything below came from the production image built from this branch's Dockerfile, running The destinations pageHourly schedule, the call-log high-water mark, and per-destination state with Test / Run now /
The config formScrolled to the payload controls. The toggle is unchecked by default; ticking it reveals the Reading the destination back through the API after creating it in the UI: {"includeBodies": true, "maxBodyBytes": 262144, "enabled": true,
"table": "…omniroute_live_e2e.call_logs_from_ui", "retentionDays": 30,
"credential": "__stored__"}The exported dataomniroute-call-logs-sample.csv Opening that same call id in the Logs detail pane returns the identical prompt and the same four Payload presence by API key in that table: The Table layout, read back from BigQueryOne dependency to be aware ofThe four SafetyThe CSV was scanned for service-account keys, bearer tokens, JWTs, provider keys and unredacted Assets live on a separate |
…t a driver line the primary path never prints (twin of diegosouzapw#12032) (diegosouzapw#12047) * fix(release): the packaged-app smoke verifies the database opened, not a driver line the primary path never prints (release/v3.8.51 twin of diegosouzapw#12032) Same change as diegosouzapw#12032 on main: the packaged app opens SQLite during the smoke but its primary open path prints no "[DB] Driver: …" line (only the recovery path and the sql.js fallback do), so the diegosouzapw#7592 assertion failed every Linux release leg. The guard rejects the sql.js fallback line, accepts a native driver line, and otherwise accepts demonstrable database activity; after readiness the smoke requests /api/monitoring/health and waits for that activity outside the readiness loop. electron-smoke-script suite 10/10. * fix(release): reapply the smoke rework on top of release/v3.8.51's own copy of the script The previous commit copied main's file wholesale and dropped this branch's ensureSmokeEnvDirs(currentPlatform) fix and its tests; this reapplies only the DB-open evidence change as a patch. electron-smoke-script suite green.
…souzapw#12048) * docs(ops): the .113 heavy-build ceiling is one runner, not two Two concurrent next-builds (15.4 GB + 17.2 GB RSS) OOM-killed one on 2026-08-29 17:26 UTC; systemd booked the kill on the other runner's unit and its job died with the same "shutdown signal" text a hosted-runner OOM shows. omni-build now lives on omniroute-113-5 only; 113-6 keeps omni-release. The janitor ceiling counts every listener on the box (4 OmniRoute + OmniHeuris + OmniMind = 6). The second heavy slot returns when the Proxmox VM gets more RAM; the exact command is in the doc. * docs(ops): apply the single-heavy-slot text (previous commit only carried formatting)
…12050) Turbopack had 31 GB on omniroute-113-6 and still panicked (TurbopackInternalError: there must be a path to a root, run 33253576569). The same tree's arm64 webpack build on hosted ARM succeeded. Dockerfile already documents webpack as the Docker escape hatch. Keep amd64 on the one omni-build slot (diegosouzapw#12048).
|
@diegosouzapw hi, pls code review. in summary i propose to have exporters to some external datasets (1st implementation - bigquery) |
diegosouzapw#11600) (diegosouzapw#11996) Fixes the blocking Lint job's own ci.yml cache: PR diegosouzapw#11963 removed the stale restore-keys fallback from quality.yml but left ci.yml's two "Restore ESLint file cache" steps carrying the same prefix-match fallback that lets a cache from a different lint config report stale per-file verdicts. Byte-level parity with diegosouzapw#11963's already-merged fix. Deliberately half of diegosouzapw#11600 — the other half (run-eslint-json.mjs) is covered by PR diegosouzapw#11983 from a parallel session, so the two don't collide on the same file.
… list (diegosouzapw#11949) (diegosouzapw#11995) Local no-API-key providers (ollama-local, lm-studio, vllm, etc.) were invisible in the Qdrant embedding-model dropdown because configuredProviders required a real apiKey or OAuth. Extended the filter to also include providerAllowsOptionalApiKey(connection.provider) — the same canonical helper already used for the identical check elsewhere. TDD: 21/21 integration tests pass (was 20/21 before the fix).
…gosouzapw#11861) (diegosouzapw#11993) Fixes a copy-paste label typo (Hermes-4-405B mislabeled "7B") in both the registry and the free-model catalog data, spotted in the diegosouzapw#11861 comment thread. TDD: 3/3 tests, generic parameter-size consistency check + exact regression guard.
… to the curated body (diegosouzapw#12096) Phase 3 creates the GitHub Release with the curated notes right after the tag push, so softprops always finds an existing body; generate_release_notes must be false on every event, not only on workflow_dispatch (v3.8.48 shipped with the auto block appended; the body sits ~3 KB under the 125,000-char cap). Same hunk as main (diegosouzapw#12086); the diegosouzapw#12085 squash did not carry it. Refs diegosouzapw#12084
…egosouzapw#11937) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Trivial, correct log-level fix — both conditions are already handled gracefully by callers. Thanks.
…iegosouzapw#11936) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Verified the console fallback → null fix removes the duplicate plain-text line while the structured pino log is unaffected. Thanks.
diegosouzapw#11923) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green. Retargeted from main to release/v3.8.51. One-line baseUrl fix with a live endpoint probe documenting the exact 404→401 transition — solid verification. Thanks.
…unks (diegosouzapw#11921) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green. Retargeted from main to release/v3.8.51. Confirmed ollamaTransform.ts was the only streaming transform not using a persistent { stream: true } decoder — matches the pattern already established in responsesTransformer.ts. TDD repro included. Thanks for finding an unreported bug.
…gosouzapw#11910) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Reviewed the security fencing closely — the status query fences on lease_owner_hash + api_key_id + generation + state=ACTIVE + not-expired, gated behind the existing lease:exclusive scope check. configuredConnectionName() correctly excludes email-derived fallback labels from the response. Test coverage explicitly verifies foreign key / different owner / stale generation all fail closed with 409, and no metadata leaks for released/expired/invalidated/missing leases. Thanks for the careful privacy-safe design.
…directory (diegosouzapw#11827) (diegosouzapw#11906) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Clean, well-scoped env-override with correct blank-value handling and a startup log naming the resolution source. Thanks.
…in manifest (diegosouzapw#11903) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Discovery-only as claimed — nothing reads the new tag yet, dashboard quota widget stays gated by USAGE_SUPPORTED_PROVIDERS. Thanks.
…lently dropped (diegosouzapw#11863) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Fixes a genuinely confusing failure mode — matches the documented TROUBLESHOOTING.md symptom exactly. Thanks for the preflight check and clear diagnostics.
…diegosouzapw#11857) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Live-reproduced root cause (byte-for-byte reproduction/removal of the malformed schema) is solid evidence. Thanks for tracing this to the builtin skill schemas rather than stopping at "provider outage".
…gosouzapw#11825) (diegosouzapw#11934) Resynced onto release/v3.8.51 (originally targeted main; retargeted since the default branch is release/v3.8.51). One real conflict in open-sse/handlers/chatCore.ts, but it was entirely unrelated to this PR's actual purpose: the antigravity-aware lockExactModel branching and deferAntigravityQuotaStateToCaller state exist on main but haven't been synced to release/v3.8.51 yet (confirmed by diffing your branch against its own main merge-base — the only change there was a Prettier reformat, not new logic). Discarded that unrelated drift and kept the release tip's current quota-lock shape; the onStreamComplete plugin wiring itself is untouched and intact. typecheck:core clean, 13/13 plugin delivery tests pass. Thanks for the thorough three-layer root-cause writeup.
…iegosouzapw#11927) Routine patch bump of a GitHub-owned action (codeql-action 4.37.7 → 4.37.8).
…uzapw#11926) Routine patch bump of a GitHub-owned action (codeql-action 4.37.7 → 4.37.8).
…egosouzapw#11925) Routine patch bump of a GitHub-owned action (codeql-action 4.37.7 → 4.37.8).
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Correção pequena e correta — `passthroughModels: true` para o Vercel AI Gateway. Validado no worktree combinado do lote (typecheck limpo, gates estáticos verdes). Obrigado!
…gosouzapw#11766) (diegosouzapw#11794) Fix correto — CLI agora sonda IPv4 e IPv6 no probe de prontidão do servidor, com teste de regressão próprio (`tests/unit/cli-waitForServer.test.mjs`). Validado no worktree combinado (typecheck limpo, teste focado verde). Obrigado!
…diegosouzapw#11784) Regeneração legítima do `package-lock.json` do workspace `packages/browser-pool`. Sem alteração de código, validado no worktree combinado. Obrigado!
…gistration (diegosouzapw#11830) Adiciona GLM-5.3-Flash ao catálogo com pricing/specs e teste próprio. Validado no worktree combinado (typecheck limpo, teste focado verde). Obrigado!
…nd GitHub wiki (diegosouzapw#11834) Resolução de links relativos entre Fumadocs e a wiki do GitHub, com dois arquivos de teste novos e bem focados (`docs-link-resolver.test.ts`, `sync-wiki.test.ts`). Validado no worktree combinado. Obrigado!
… chart tooltips (diegosouzapw#11960) Fix de contraste no tooltip do gráfico de custos (fundo opaco + cor de texto legível). Mudança isolada de CSS/classe, validada no worktree combinado. Obrigado!
…form/pluginType as numeric enums) (diegosouzapw#11969) Envia metadata completo do loadCodeAssist (ideType/platform/pluginType como enums numéricos) para o Antigravity, com teste de regressão atualizado. Validado no worktree combinado. Obrigado!
…et (diegosouzapw#11959) Dá aos alvos de extended-thinking o orçamento de prontidão de reasoning, com cobertura de teste ampliada em `stream-readiness-policy.test.ts`. Validado no worktree combinado. Obrigado!
…diegosouzapw#11854) Corrige divergência de scoring no relatório de saúde do auto-combo, com testes atualizados em `combo-resolve-auto-strategy-split.test.ts` e `combo-scoring-inspector.test.ts`. Validado no worktree combinado. Obrigado!
Adiciona 5dive como configure target, com teste de regressão próprio (`tests/unit/cli/setup-5dive.test.ts`) e strings i18n em 12 locales. Validado no worktree combinado (typecheck limpo, 26 testes focados).
Nota: um dos subtestes desse arquivo ("falls back to the local server when no context") depende de não haver contexto CLI ativo em `~/.omniroute/` — nesta máquina de desenvolvimento compartilhada existe um contexto real configurado, então o teste lê a config real em vez do fallback via `PORT`. Confirmado que é vazamento de ambiente do devbox (não do CI): reproduzido isoladamente, rastreado até `resolveActiveContext()` lendo `~/.omniroute/*.json` antes de cair no fallback de `PORT`. Não bloqueia o merge, mas fica registrado — o teste merece ficar hermético (mockar/isolar o data dir) numa limpeza futura.
…11970) Mostra a % de cache nos logs de requisição, com teste próprio (`request-logger-cache-percentage.test.ts`). Validado no worktree combinado do lote. Pequeno ajuste feito por cima: `formatCachePercentage` movida de `RequestLoggerV2.tsx` para `src/shared/utils/formatting.ts`. `RequestLoggerV2.tsx` importa `RequestLoggerDetail`, que desde o diegosouzapw#11703 (mergeado nesta mesma sessão) importa CSS bruto de `react18-json-view` — algo que o Node native test runner não consegue carregar. O teste original importava a função direto do componente e quebrava por causa dessa cadeia de import, não por bug na PR. Movida a função (pura, sem dependências) para o utils compartilhado; ajustado o import do componente e do teste. Typecheck limpo, teste passando (6/6).
385e90f
into
diegosouzapw:release/v3.8.51
… (BigQuery first) (diegosouzapw#11945) Feature grande e bem construída: exportação contínua de call logs para destinos plugáveis (BigQuery primeiro). Revisei especificamente o tratamento de segredos (`src/lib/logExport/secrets.ts`) e a migração — encryption gate real (`requiresEncryptionKey` recusa gravação em texto plano quando `STORAGE_ENCRYPTION_KEY` não está setada), redação antes de qualquer resposta de API, e a migração cria a tabela com `enabled=0`/`include_bodies=0` por padrão (opt-in, sem exportar nada até o operador configurar). 62/62 testes focados verdes, typecheck limpo. Resolvido o conflito com o barrel `src/lib/localDb.ts` (removido nesta mesma sessão, diegosouzapw#11795 fase 5 — todo consumidor já migrado para `src/lib/db/*`); a PR só adicionava um re-export nele, que não é mais necessário. Obrigado pela contribuição!


Summary
The Logs tab keeps request history in SQLite, bounded by rotation and retention, so anything older than the window is gone. This adds a scheduled export that ships those same records to an external store before that happens.
A destination is a plugin. It declares a Zod config schema, a field descriptor the dashboard renders its form from, which config keys are secret, and a client with
test()/prepare()/send(). Adding Datadog or Grafana Loki later means one file undersrc/lib/logExport/destinations/and one line in the registry. The runner, the REST layer and the UI never learn what a destination is.Google BigQuery is the first one. It talks the REST API directly with a self-signed RS256 assertion rather than pulling
@google-cloud/bigqueryandgoogle-auth-libraryinto the tree, which is about 100 lines ofnode:cryptoandfetchand keeps the dependency count where it was.Some details worth knowing:
call_logs.rowid, nottimestamp.saveCallLogaccepts a caller-supplied timestamp, so a slow request can be written after a faster one that started later, and a timestamp cursor would step over it. A cursor aboveMAX(rowid)means the table was purged and rowids restarted, so the runner rewinds instead of going permanently blind.send()resolves. A failed batch is retried on the next run. The guarantee is at-least-once plus destination-side dedup, not exactly-once, and every row carries the call-log id as itsinsertIdso a resent batch collapses on BigQuery's side.insertErrors[]. That throws. Treating it as success is how you advance the cursor past rows BigQuery never took.send()chunks on row count and serialised bytes, closing at 500 rows or 9 MB, whichever comes first. Row count alone is not enough once payloads ship: 500 rows carrying prompts can be tens of megabytes against insertAll's 10 MB cap.encrypt()is a silent passthrough withoutSTORAGE_ENCRYPTION_KEY, storing a destination that carries a credential is refused with a 400 when the key is absent, following the guard the Telegram webhook already uses.Payloads are a separate, opt-in step. By default the export carries the summary fields the Logs list shows. A destination can set
includeBodiesto additionally ship what the Logs detail pane shows: the request and response bodies plus the pipeline's route decision, client raw request, normalised OpenAI request, provider-dialect request, provider response and client response. This is prompt content, so it is off by default and chosen per destination. Both the export and the dashboard read throughgetCallLogById, so the two cannot drift, and payloads arrive already PII-sanitised and secret-redacted withnoLogcalls carrying none.The auto-created BigQuery table is day-partitioned on
timestampand clustered onapi_key_name, provider, model, status, ordered by how call logs are actually filtered. Partition retention is configurable. Both apply at creation, so an existing table keeps its layout.New env var:
OMNIROUTE_LOG_EXPORT_CRON, default0 * * * *. Docs indocs/frameworks/LOG-EXPORT.md.A build failure this PR introduced, and the panic that hid it
Every page under
docs/is compiled by fumadocs-mdx, whose schema requires atitle. The newdocs/frameworks/LOG-EXPORT.mdshipped without frontmatter, sonpm run buildfailed:Turbopack never prints that message. It panics inside
ImportTracer::get_traceswhile buildingthe import trace for the issue it is about to report, so the build dies with
internal error: entered unreachable code: there must be a path to a root(
turbopack-core/src/module_graph/mod.rs:746) and the cause is swallowed. Building with thedocumented escape hatch
--build-arg OMNIROUTE_USE_TURBOPACK=0surfaces it immediately.Frontmatter added; the production Docker image now builds and runs. The Turbopack panic itself is
independent of this change and still reproduces on the base tip with the frontmatter fixed in
place, so it is an upstream bug worth its own issue, not something this PR can address.
Related Issues
Validation
npm run lintrelease/v3.8.51)Full CI on the current head is green: all four unit shards, Fast Quality Gates, Docs Gates, ESLint, Vitest, Merge integrity and both semgrep jobs.
Two gate failures from the first push are fixed here.
check:db-rulesrequires everysrc/lib/db/module to be re-exported fromlocalDb.ts, whichlogExportDestinationswas not; and the locale parity suites require everyen.jsonkey to exist in all 42 locales, sosidebar.logExportandsidebar.logExportSubtitleare now translated rather than English-only.Live validation: the production image, a real provider, a real BigQuery table
Built the production image from the repo Dockerfile (
--target runner-web), ran it, pointed it ata real Cursor API connection and a real BigQuery dataset, and drove the whole path through the
shipped HTTP surface. 21 checks, all green, repeatable on a fresh table name each run:
A sample exported row, showing the null and boolean handling that matters:
{"id":"1787997867123-e1ba1e","model":"auto","requested_model":"cursor-api/auto", "provider":"cursor-api","status":200,"tokens_in":56,"tokens_out":25, "tokens_cache_read":null,"model_pinned":false,"api_key_name":"log-export-e2e", "error_summary":null,"duration_ms":2743}Separately, to prove the schedule fires rather than merely being registered, a second container ran
with
OMNIROUTE_LOG_EXPORT_CRON="* * * * *". One live call, then nothing was triggered by hand:the job ran on its own,
job_runsrecordedstatus=success, and the rows landed in BigQuery.Two defects from earlier runs against the real API are fixed here: a table created moments before is
not yet visible to the streaming endpoint and answers 404 for a few seconds, so that 404 is retried
but only when the run itself created the table; and the retry's sleep used an unref'd timer, which
let the process exit out from under an in-flight retry.
Tests Added Or Updated
tests/unit/log-export-runner.test.ts(new) cursor advance, batching, retry after failure,max_rows_per_run, purge rewind, concurrent-run guard, migration 170 upgrade path, and for payloads: nothing ships withincludeBodiesoff (asserted against the wire, not the record), both client and provider views ship when on, oversized payloads truncate and flag, a missing artifact exports its summary instead of failing the batchtests/unit/log-export-bigquery.test.ts(new) field mapping vs table schema, insertId dedup,insertErrorson HTTP 200, chunking at 500, transient retry, terminal 403, fresh-table 404, token cache, clustering and partition retention on the created table, byte-aware chunking under multi-megabyte payloads, and a single oversized row still shipping rather than wedging the cursortests/unit/log-export-routes.test.ts(new) CRUD, validation, 404s, secret redaction, placeholder round trip, encryption-key guardtests/unit/log-export-secrets.test.ts(new) encrypt, decrypt, redact, mergetests/unit/log-export-extensibility.test.ts(new) a fake destination drives the whole runner with no BigQuery code loaded, plus a grep assertion that no destination-specific vocabulary leaked into the 17 generic filestests/unit/sidebar-visibility.test.tsupdated for the new sidebar entryCoverage Notes
Everything under
src/lib/logExport/,src/lib/db/logExportDestinations.ts,src/lib/usage/callLogExportSource.ts,src/lib/jobs/logExportJob.tsandsrc/app/api/log-export/is covered by the five new suites, including the failure paths. The dashboard components undersrc/app/(dashboard)/dashboard/log-export/have no unit tests and follow the existing dashboard pages in that respect.Reviewer Notes
170_log_export_destinations.sqladds one table and one index. Nothing else is touched, and the upgrade path is covered by a test that drops the table and its ledger row and reopens the database. This takes the repo to 167 migrations, so the three docs that hardcode the count and the 42llm.txtmirrors are updated.initCloudSync, soOMNIROUTE_DISABLE_BACKGROUND_SERVICESand the automated-test guard already switch it off. A fresh install has no destinations and exports nothing.includeBodies, off by default). When on, the export additionally carries the request and response bodies plus the pipeline's client and provider views, read throughgetCallLogByIdso it cannot drift from the Logs detail pane. They arrive already PII-sanitised and secret-redacted, and a call made with anoLogAPI key stores none, so there is nothing to export.maxBodyBytestruncates rather than drops and flags the row. Streamed chunk deltas are not exported.__registerLogExportDestinationTypeForTest/__reset...), matching the pattern already used by__resetJobRegistry. They exist so the extensibility test can prove the pipeline works with no BigQuery code loaded. Happy to drop them if you would rather keep the registry closed.POST /destinations/{id}/runreturnsskipped: truerather than a 409 when a run for that destination is already in flight. Easy to change if you prefer the status code.