Repository navigation
feat(backend): THINKER_CATALOG_SYNC and OpenCode Go doc only with a provider (#39) - #72
Conversation
Thinker is self-hosted only, so the inherited Manifest Cloud UI goes. The frontend leaves first so no screen breaks when #14/#15 drop the matching backend endpoints. - Billing/plans: delete PlanPicker, UpgradeSuccessModal, UsageLimitBanner, the /upgrade page, the Register ?step=plan picker, the plan stores, the plan-based range locks on the analytics pages, the plan-limit upgrade CTAs in the request views, the Account billing and usage-email sections, and the @better-auth/stripe client plugin and dependency. - API Bot: delete the banner, the sidebar card, the dithered ground, their stylesheet and the SVG asset. - GitHub stars: drop the header star button and its /api/v1/github call. - Free Models: delete the page, its route, its API client and the assets only it used. - Managed Gemini Free: drop the provider entry, icon alias, background /ensure provisioning call and the Calendly key URL. - Cloud branches: checkIsSelfHosted() is gone and every screen behaves as self-hosted (Local providers always listed, local logs copy, CLI login pinned to this host, discovery after signup and setup). Drop the Manifest Cloud origin mapping, the cloud email card, the VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it. - Help: drop the Manifest Calendly booking link. - Remove the dead CSS for all of the above, update the tests and the inherited dev guide. Closes #12 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Billing, Stripe and the Free/Pro plans belong to the SaaS layer that Thinker drops (docs/decisoes-iniciais.md section 3). - Delete packages/backend/src/billing (PlanService, billing controller, billing emails, request quota window, subscription status) and drop BillingModule from the app, proxy and analytics modules. - Proxy: remove the per-request plan gate (assertWithinRequestLimit) and the PLAN_LIMIT_REQUESTS / M204 handling in the controller and the exception filter. The proxy no longer answers 402 on its own; a 402 follows the generic error path. - Auth: remove the @better-auth/stripe plugin; drop the stripe and @better-auth/stripe dependencies from the backend. - Recording retention: keep only the global path (365-day default or REQUEST_RECORDING_RETENTION_DAYS); per-plan retention is gone. - Tenant entity: drop limit_overrides and billing_email_preferences; delete the TenantRequestUsage entity. - Shared: delete plan-limits and its exports. - Remove the STRIPE_* / PLAN_LIMIT_* / PLAN_REQUEST_QUOTA_RESET_AT variables from .env.example. - New migration DropBillingSchema1803200000000 drops the quota trigger and function, requests.quota_counted, tenant_request_usage, billing_email_logs and the two tenant columns. The old AddTenantRequestUsage migration keeps only its self-hosted path (a no-op up) so it no longer imports deleted billing code. /api/v1/billing/* no longer exists, and the STRIPE_* and PLAN_LIMIT_* variables are ignored if set. Closes #14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thinker makes no outbound calls of its own, and Autofix depended on Manifest's hosted Phoenix service. The dashboard drops both before the backend modules go in #16 and #17. - Update check: VersionIndicator now only shows the build-time __MANIFEST_VERSION__ badge. It makes no request, shows no "new version" link to the manifest.build changelog and has no dismiss state. services/api/version.ts and its test are gone. - Discovery: delete the post-signup questionnaire (page, service, styles, route, tests) and the pending-discovery redirects in AuthGuard and GuestGuard. First-admin setup now lands on / (Overview), signup on /welcome, and social signup also goes to /welcome. A leftover manifest_discovery_pending_* key from an older build no longer redirects anyone. - Wingman: delete the dev drawer, wingman-dev-proxy.ts, the VITE_WINGMAN_* / __WINGMAN_URL__ / __DEV_MODE__ build constants, the App and Welcome mounts, the drawer CSS and the CONTRIBUTING section. - Autofix: delete AutofixKpiCards, SettingsAutofixSection, NotificationBell (it only nagged about inactive Autofix), the Autofix switch and Manifest Terms/Privacy line on harness creation (modal and Welcome), the autofix badges, trigger filter, Phoenix panels, sibling links and status labels in the Requests views, the "Autofixed attempts" connection card, the Doctor tooltip variant, the healed chart series, the autofix fields in services/api/*, the two public PNGs, and the dead CSS. - The generic reliability metrics that the backend still serves under the legacy /overview/autofix-* paths (success rate, fallback recoveries, request status over time, per harness/provider/model reliability) stay. They are now read through neutral clients (getReliabilityStats, getRequestStatusTimeseries, RELIABILITY_PATHS) and shown by ReliabilityKpiCards. "Self-healed" becomes "Recovered" and counts fallback recoveries only. - Update the tests, the design-system registry and the inherited dev guide. Closes #13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up for #57. - A harness stored without autofix_enabled inherits the backend default. The backend turns Autofix ON when it detects no container (bare-metal npm start, dev runs), and the dashboard no longer has a switch to turn it off. Until #16 removes Autofix from the backend: - createAgent always sends autofix_enabled: false, which covers the Connect Harness modal, Welcome and ConnectProvider; - duplicateAgent follows the copy with PATCH /routing/:agent/autofix {enabled:false}, because the duplicate endpoint has no field for it. Both live in one commented block in services/api/agents.ts, with tests on the request payloads. - The request-status timeseries client folds a legacy `healed` key into `success`. The backend's success rate already counts those requests as successes, so the bars still sum to the request total, Success + Recovered + Error matches the Success rate card, and Recovered stays fallback-only, like its KPI. - The Requests page rewrites a stale ?trigger= value (for example ?trigger=autofix) on load to the filter it actually applies. - Add AuthGuard tests for /cli/auth, with and without a session. - Correct the dev guide: the self-hosted default is OFF only in a container or with MANIFEST_MODE=selfhosted. Document AUTOFIX_GLOBAL_ENABLED=false as the operator workaround for existing harnesses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tHub stars and error pages Remove the Manifest Cloud and marketing surfaces from the backend: - waitlist/ (pivot and Autofix claims) and its open-CORS pivot route (applyPivotClaimCors, PIVOT_CLAIM_PATH, PIVOT_CLAIM_CLOUD_ORIGIN). - crm-metrics/ and CRM_METRICS_SECRET. - The managed Gemini Free provider: routing/managed-free-provider/, common/constants/managed-free-providers.ts, the CREDITS_* variables, and its entries in provider-endpoints, provider-client, provider.service, provider-model-fetcher and routing.module. No other provider's endpoint, prefix handling, fallback order or tier resolution changes. - free-models/ (sync from mnfst/awesome-free-llm-apis), github/ (stars from api.github.com) and error-pages/ (public and Peacock push routes, seeders, discovery), MANIFEST_PUBLIC_STATS and ERROR_PAGE_PUSH_SECRET. - gemini-free and the managedFree flag from manifest-shared, and the regenerated CLI provider catalog. - The plan_request_limit_exceeded error class left over from #14. DropSaasFeatureSchema1803300000000 sets a transaction-local lock_timeout, drops waitlist_claims, public_error_pages and the cloud-only IDX_requests_autofix_healed index, and deletes every gemini-free connection with its agent grants. Request history keeps its provider label, and routes that still name gemini-free resolve as unavailable and fall through to their fallbacks, as after a disconnect. down() restores the two tables as the self-hosted path created them. Closes #15 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Autofix sent failing requests to Manifest's hosted healer (autofix.manifest.build) and pinged it at every production boot. Thinker makes no such call (#16). The local self-heal of v0.6 will build on the fallback chain on its own terms. - Delete routing/autofix (AutofixService, healing clients, health probe, observation reporter, Phoenix types and vendored contract) and the contract:refresh script with its ajv/js-yaml devDependencies. - Proxy: drop the heal-once branch from the primary and fallback hops and the wire-body retry hook from ForwardResult. A failed request now goes straight to shouldTriggerFallback, exactly as for an agent that never opted in. The primary is always attempt 1, the x-manifest-provider / x-manifest-auth-type replay rewrite is gone, and the recorder writes no repair columns or auto_fixed rows. - Analytics: move the generic reliability metrics to /api/v1/overview/reliability/{stats,timeseries,per-agent,per-provider, per-model} without the Autofix fields; drop the healed disposition, the auto_fixed counter, the autofix trigger filter and the connection breakdown's Autofix attempts. - Remove /autofix/status, /autofix/enable-all, the per-harness /routing/:agent/autofix endpoints, the autofix_enabled create field, the MCP Autofix tools and the CLI `routing autofix` command and `--autofix`. - Schema: DropAutofixSchema1803400000000 drops the autofix columns, check constraint and indexes (lock_timeout, IF EXISTS, full revert). 1802200000000 now runs its self-hosted no-op path everywhere. - Shared: drop deriveAutofixStatus, AUTOFIX_STATUSES and auto_fixed. - Delete the unused doctor-release email. - Frontend: call the neutral reliability paths and drop the temporary autofix_enabled opt-out, the duplicate PATCH and the healed fold. - Docs: move the PoC spec to docs/referencia/, remove AUTOFIX_* from the guide, .env.example and the Docker README. BREAKING CHANGE: the Autofix API, analytics paths, MCP tools and CLI command are removed; the reliability analytics moved to /api/v1/overview/reliability/*. Closes #16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thinker makes no outbound call of its own. Remove every upstream phone-home from the backend and the CLI: - usage telemetry (telemetry/, InstallIdService, PayloadBuilderService, the InstallMetadata entity) and the CLI usage spool (packages/cli/src/telemetry.ts); - the daily update check (version/, GET /api/v1/version, the GitHub releases URL); - the discovery forwarding to blue.manifest.build (discovery/, POST /api/v1/discovery/complete); - Sentry (instrument.ts, sentry/, SentryModule/SentryGlobalFilter, the @sentry/nestjs dependency); - the hosted Wingman origin in CORS and CSP frame-src; frame-src is 'self' in every mode and WINGMAN_CORS_ORIGINS becomes THINKER_CORS_ORIGINS with the same meaning; - the modelparams.dev fetch on boot and hourly cron; parameter specs come only from the bundled modelparams package snapshot. DropInstallMetadata1803500000000 drops the install_metadata table (lock_timeout, IF EXISTS, revert recreates it empty). The removed variables leave docker-compose, the .env examples, the Docker README, the deploy templates and the developer guide. A new e2e guard fails if the backend, CLI or shared source names a removed destination again, or if a production-mode boot of the real server fetches or resolves anything outside the ADR 0003 catalogs. BREAKING CHANGE: GET /api/v1/version and POST /api/v1/discovery/complete are gone, the hosted Wingman origin is no longer allowed by CORS, and WINGMAN_CORS_ORIGINS is renamed THINKER_CORS_ORIGINS. Closes #17 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no --url, MANIFEST_URL or stored login, the CLI targeted https://app.manifest.build, so a MANIFEST_API_KEY set without a host went to Manifest's cloud. DEFAULT_URL is now http://localhost:2099, the default Thinker port, and the invalid-URL hint and README follow. The backend's 404 hint for /chat/completions told callers to point their baseURL at https://app.manifest.build/v1; it now refers to this gateway. The egress guard now fails on app.manifest.build or gateway.manifest.build anywhere in the CLI or shared source (tests included), on a new backend production file naming them, on a CLI default that is not local, and on a boot request to either host. The three remaining backend files (Cloud trusted origins, the dashboard link fallback and the OG-tag placeholder) are pinned for #18, #22 and #25 and may only shrink. Refs #17 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thinker has one deployment mode. The backend no longer decides between "cloud" and "self-hosted" from MANIFEST_MODE or container markers (/.dockerenv, /run/.containerenv, KUBERNETES_SERVICE_HOST); it always keeps the former self-hosted behavior, on any runtime: - built-in local providers (Ollama, LM Studio, llama.cpp) are always available: the cloud-only filters, the M303 rejection and its error code are gone; - custom-provider URLs validate with allowPrivate: LAN, loopback and http:// destinations are a feature, http:// to a public host is still refused, and cloud metadata endpoints stay blocked (now also by name: metadata.google.internal, metadata.goog, metadata, instance-data); - request recordings fall back to the filesystem without S3 credentials; - the boot backfills always run on the app's DATABASE_URL connection; - GET /api/v1/setup/status drops isSelfHosted and always probes Ollama. The CIMD client_id fetch does NOT inherit the old self-hosted allowance: that URL comes from an unauthenticated MCP client, so it is public-only by default (DNS failure is an error), with THINKER_CIMD_ALLOW_PRIVATE=true as the opt-in for LAN-hosted clients. auth.instance.ts no longer trusts the Manifest Cloud hosts (app/gateway.manifest.build) as origins or MCP resources; MCP is bound to the canonical BETTER_AUTH_URL origin only, and the file leaves the no-phonehome allow-list. detect-self-hosted.ts becomes container-host.ts with only getContainerHostAlias(). Unit tests no longer depend on /.dockerenv. Closes #18 Closes #46 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on a provider Implements ADR 0003 (#39): - THINKER_CATALOG_SYNC=false (also 0/no/off) turns off all three public catalog fetches: no boot fetch, the daily crons and the pricing/model refresh paths become no-ops, and the OpenCode Go catalog returns [] without fetching. OpenRouter and models.dev stay on by default. - OpencodeGoCatalogService only warms the raw.githubusercontent.com doc at boot when a tenant_providers row for OpenCode Go (id or alias) exists. Every other caller is already scoped to an OpenCode Go provider, so a newly registered provider loads the doc lazily through its discovery. - pricing-health and pricing/refresh report sync_enabled, and the routing status (REST and MCP) no longer blames the OpenRouter cache, which is empty by design with the sync off. - The egress e2e boots the real server three times: no OpenCode Go provider (OpenRouter and models.dev fetched, no raw.githubusercontent.com), provider + THINKER_CATALOG_SYNC=false (no remote fetch at all), and provider + default (the doc is warmed). - Documents the variable in both .env.example files, the compose file, DOCKER_README and the dev guide, including what pricing loses when the sync is off. Closes #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revisão do PR #72 (#39,
|
Review of #71 (S1): with allowPrivate now constant for custom providers, a DNS failure passed validatePublicUrl, and fetch then resolved the name again with no check. A hostile resolver could answer a public address (or fail) during validation and 169.254.169.254 at connect time, so the probe and the forward could reach cloud metadata. - New common/utils/ssrf-safe-fetch.ts: undici fetch with a shared Agent whose connect.lookup resolves once, refuses the connection when any answer is a metadata address, and hands the socket only validated addresses. Metadata literals and names are refused before any socket. A DNS failure fails the request. - The custom-provider probe and the provider-client forward for user-supplied endpoints (requiresSsrfRevalidation) use it. - validatePublicUrl treats a DNS failure as an error even with allowPrivate (a .invalid host is refused). - S2: block the AWS container credential endpoints 169.254.170.2 (ECS), 169.254.170.23 and fd00:ec2::23 (EKS Pod Identity). - N2: classify IMDS embedded in IPv6 (IPv4-compatible, NAT64 64:ff9b::/96, 6to4, Teredo) as metadata; IPv6 metadata matches any textual form. - undici 6.29.0 becomes a backend dependency (the root override already pins undici ^6); Node's built-in fetch cannot take a foreign Agent across Node versions, so undici's own fetch is used. Tests: unit specs for the lookup and the classifier, and an e2e where a hostname resolves to a public address at validation and to 169.254.169.254 at connect: the probe and the forward are refused and a listener standing in for the metadata address receives 0 connections (it receives them when pinning is removed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #67 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #67 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 498de23 (main plus #60) so #16 can merge into main after #60. Conflicts came from #57's interim dashboard handling of the legacy Autofix backend overlapping with this branch's backend removal: - frontend analytics client: RELIABILITY_PATHS points at the neutral /overview/reliability/* routes. foldLegacyRecoveredKey is dropped: the backend computes the request disposition from success/fallback only and DropAutofixSchema removes requests.autofix_status, so no row can come back as "healed", and the dashboard ships with its own backend. - frontend agents client: the auto-merged autofix_enabled opt-out on create and the PATCH /routing/:agent/autofix after duplicate are dropped (the field and the endpoint no longer exist; create rejects the field). - shared: keep the playground-headers exports, drop the autofix-status ones. - tests: keep this branch's versions (neutral paths, no opt-out, no fold). - e2e specs from main (local providers, reserved local runner aliases): stop sending autofix_enabled on agent create. - manifest guide: keep the "Autofix removed" section and neutral-path wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DropAutofixSchema e2e spec opens its own DataSource with dropSchema, so it now takes its URL from resolveE2eDatabaseUrl() like the other migration specs instead of falling back to a default localhost database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-phonehome-backend Brings in main (44f1689) through #66's final head so #70 merges cleanly after #66. Conflict resolutions: - packages/backend/package.json, package-lock.json: keep #70's removal of @sentry/nestjs and take better-auth 1.7.7 from #5. The lockfile was re-checked with npm install --package-lock-only and needed no change. - test/cors.e2e-spec.ts: keep #70's shared buildApp/buildDevApp/buildProdApp without the hosted Wingman origin; buildApp already serves on loopback (#77). - test/drop-autofix-schema-migration.e2e-spec.ts: keep both the DropInstallMetadata revert from #70 and resolveE2eDatabaseUrl from #66. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se guard The DropInstallMetadata migration spec and the no-phone-home egress spec both open a dropSchema connection. Resolve their URL with resolveE2eDatabaseUrl() from test/e2e-db-guard instead of falling back to localhost:5432, so neither can wipe a database that is not an explicit, disposable test database (#67, #69). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's tree equals 5eef655, already merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le-selfhosted-mode Brings in main (#5, #63, #69, #100, #103, #104, #106, #107 and the SaaS, billing, Autofix and phone-home removals) through #66 and #70. Resolutions: - package-lock.json: keep undici 6.29.0 as a runtime dependency (no "dev": true), since ssrf-safe-fetch imports it; the version bump from #65 is kept. - src/mcp/mcp-discovery.spec.ts: keep the single-mode test (canonical resource and issuer whatever the Host header) and serve it on loopback like the rest of the spec (#77). - src/common/constants/local-servers.ts and its spec: import getContainerHostAlias from common/utils/container-host, since detect-self-hosted no longer exists. - design-system/registry: regenerate (npm run design:registry). The Playground header classes from #106 were missing from it, which the pre-commit check refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… local log flake - Remove the MANIFEST_MODE=selfhosted setup that #100, #103 and #104 added to provider-client.local-servers.spec.ts, local-providers.e2e-spec.ts, reserved-local-runner-aliases.e2e-spec.ts and playground-request-headers.e2e-spec.ts. There is one mode now, so the variable does nothing. - local-providers.e2e-spec.ts: "logs local requests under the canonical provider at zero cost" read rows[0] of every llama.cpp row for the agent, with no ORDER BY, while earlier tests also log llama.cpp requests and a row starts as `pending` before its usage lands. Tag the request with its own traceparent and wait for that one settled row. Closes that part of #101. - mcp-cimd-ssrf.e2e-spec.ts: take the database from resolveE2eDatabaseUrl() instead of falling back to localhost:5432 before dropping the schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL flagged js/incomplete-sanitization: only dots were escaped before building a RegExp from the host constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #72 up to date with main 90598c0 plus #71 (single self-hosted mode, ssrf-safe-fetch, local servers, playground headers, the e2e database guard, the removals, x-api-key and better-auth 1.7.7). Conflicts: - docker/.env.example: keep both new sections, #71's THINKER_CIMD_ALLOW_PRIVATE and #72's THINKER_CATALOG_SYNC. - test/no-phonehome-egress.e2e-spec.ts: keep #72's three-boot layout (default, OpenCode Go provider + THINKER_CATALOG_SYNC=false, OpenCode Go provider + default) and route every connection, including the dropSchema reset and the spawned server's DATABASE_URL, through resolveE2eDatabaseUrl() instead of a localhost:5432 fallback. #71's removal of auth.instance.ts from the hosted-gateway leftovers is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-revisão do PR #72 (#39) no head d044aea, agora com base em
|
qa-docker: passed (with 2 notes)Tested SHA: Egress (tcpdump in the app network namespace, decoded with tshark: DNS names, TLS SNI, destination IPs)Capture started before the app's first log line in every run. Each scenario covers boot, first-admin setup, a signed-in headless Chromium dashboard session and idle time.
Notes on the capture:
Functional
Notes (not blocking)
|
Closes #39
Implements ADR 0003.
What changed
THINKER_CATALOG_SYNC. A new helper,isCatalogSyncEnabled()inpackages/backend/src/common/utils/catalog-sync.ts, reads the variable at call time. It is on by default, andfalse,0,nooroffturn it off. When it is off, all three catalog sources stop:PricingSyncService) and models.dev (ModelsDevSyncService): no boot fetch, andrefreshCache()returns 0 without fetching. That covers the daily crons,POST /api/v1/routing/pricing/refresh, and the models.dev refresh that manual model discovery runs.whenInitialized()resolves immediately, so the pricing cache warmup isn't held up.OpencodeGoCatalogService):list()andrefresh()return[]without fetching. The cost per request then becomesnull, which is recorded as $0. The API format falls back to the model-family heuristic inprovider-client.ts.OpenCode Go only with a registered provider. The boot warmup (
onModuleInit) now fetches only when atenant_providersrow exists whose provider isopencode-goor its aliasopencodego, compared case-insensitively. Inactive rows count too. With no repository, or if the lookup fails, it skips the warmup rather than reach out. Every other caller is already scoped to an OpenCode Go provider:ProviderModelFetcherService, during that provider's discoveryprovider-client, when choosing the format for anopencode-goendpointopencode-gopublishedOpencodeGoIds, gated on OpenCode Go modelsModelControllerRegistering an OpenCode Go provider runs its discovery, which loads the doc lazily and caches it as before (1 h, 5 min backoff on errors).
Status hints. With the sync off, the OpenRouter cache is empty by design. As a result:
GET /routing/:agent/statusno longer reportspricing_cache_empty.manifest_routing_statustool no longer reportsenabled: false, reason: 'pricing_cache_empty'. Before this change it did, even for agents that could route.pricing-healthandpricing/refreshnow includesync_enabled. This is an additive field.Egress guard (
test/no-phonehome-egress.e2e-spec.ts). The test now boots the real production-mode server three times against the same disposable DB:openrouter.aiandmodels.devare fetched, so the default really is on.raw.githubusercontent.comis not fetched. Nothing is fetched outside ADR 0003, and no remote DNS lookups happen.THINKER_CATALOG_SYNC=false: no remote fetch at all.raw.githubusercontent.comis warmed. This is the positive control for the gate.egress-recorder.cjsnow writes abootmarker, so run 2 can prove the recorder was loaded even though it records no fetch. I also checked the test against the bug it guards: with the provider check and the OpenRouter gate removed, exactly the two new assertions fail.Docs.
docker/.env.exampleandpackages/backend/.env.examplegained the variable.docker/docker-compose.ymlnow passesTHINKER_CATALOG_SYNC=${THINKER_CATALOG_SYNC:-true}to the container. The compose file lists its environment explicitly, so without this line the variable would never reach the container. This is a small touch in devops territory.docker/DOCKER_README.md, I added an env table row and rewrote the "No telemetry" section, which wrongly implied that raw.githubusercontent.com is always contacted. It now has a destination/trigger table and explains the opt-out.docs/dev/manifest-guide.md("No phone-home"), I added the destination table, the opt-out semantics, the losses below, and the guard's three runs.Pricing and cost display with the sync off (ADR 0003 accepted this)
I checked this in the code:
ModelPricingCacheServiceis built only from the OpenRouter and models.dev caches plus custom providers' own prices. With both caches empty, a request to any other non-local model getscost_usd = NULL("price unknown"). The exception is a provider that reportsusage.costitself. The dashboard'sformatCostshows NULL as$0.00, and totals add it as 0. Subscriptions, local providers and custom providers behave as before./modelsreturns, and the format comes from the family heuristic.known-model-prices.ts, which today covers only 3 prefixes, and toknown-model-modalities.ts. Most models show no price in the picker, and models.dev capability flags are missing. Some providers get their model list from the catalogs when their own API can't supply one, for example a Gemini subscription (CodeAssist) or a provider whose/modelsis unreachable. These may list no models.known-model-prices.tsfeeds discovery only, notcomputeTokenCost. If we want recorded costs with the sync off, that needs its own issue.Gates (local; CI pending #38)
packages/sharedbuild (tsccjs + esm)packages/sharedunit testspackages/backendlint (eslint src/)packages/backendtypecheck (tsc --noEmit) andnest buildpackages/backendunit tests (jest)packages/backende2e, in band,TZ=UTC, disposable Postgres 16 (postgres:16-alpineon 127.0.0.1:55439, dbprov39_test)Runtime behavior changed, so
qa-dockershould validate the image before this leaves draft. Suggested checks:THINKER_CATALOG_SYNC=false, the image makes no catalog traffic and the pricing refresh reportssync_enabled: false.No live keys were used. The catalog fetches in tests are mocked or blocked by the egress recorder.
🤖 Generated with Claude Code