From 21af34405452e9bd6e5b8778aa0ce206d171416f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 30 Oct 2026 17:08:43 +0100 Subject: [PATCH 1/4] fix(ai): send max_completion_tokens to Azure OpenAI reasoning deployments An OpenAI-compatible gateway entry pointed at an Azure OpenAI v1 endpoint was treated as a generic gateway and sent max_tokens and temperature, which a reasoning deployment refuses with a 400. An Azure v1 endpoint now takes the api.openai.com contract for a reasoning model, and any gateway that refuses max_tokens for a model is asked again with max_completion_tokens and remembers it, which covers deployments named something of their own. Refs #1304 --- docs/integrations/ai-providers.md | 12 + .../ai/__tests__/azure-token-budget.test.ts | 207 ++++++++++++++++++ src/lib/ai/json-dialect.ts | 6 +- src/lib/ai/local-client.ts | 2 + src/lib/ai/openai-capabilities.ts | 74 ++++++- src/lib/ai/openai-client.ts | 12 +- src/lib/ai/wire-refusal.ts | 28 ++- 7 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 src/lib/ai/__tests__/azure-token-budget.test.ts diff --git a/docs/integrations/ai-providers.md b/docs/integrations/ai-providers.md index 84e9d20f0..f099f6fb6 100644 --- a/docs/integrations/ai-providers.md +++ b/docs/integrations/ai-providers.md @@ -216,6 +216,18 @@ JSON surfaces send the standard the field is detected on the first refusal and retried without it, so strict gateways and older local servers both work unmodified. +**Azure OpenAI.** Point the gateway at the resource's v1 endpoint, +`https://.openai.azure.com/openai/v1`, and put the +deployment name in Model. A reasoning model (gpt-5 on, the o-series) +takes `max_completion_tokens` and no `temperature`, the same as on +`api.openai.com`. HealthLog sends that from the first request when the +deployment is named after its model (`gpt-6-luna-eu`). A deployment +with a name of its own (`prod-chat`) gets the usual `max_tokens` +once; Azure refuses it, and the request goes out again with +`max_completion_tokens`. HealthLog remembers that for the endpoint and +deployment until the container restarts. Any other gateway that +refuses `max_tokens` the same way is handled the same way. + **When the gateway refuses.** OpenRouter answers a refused generation with HTTP 200 and the reason inside the body, so every refusal used to reach the provider health card as an empty reply with no status. It now diff --git a/src/lib/ai/__tests__/azure-token-budget.test.ts b/src/lib/ai/__tests__/azure-token-budget.test.ts new file mode 100644 index 000000000..685513d85 --- /dev/null +++ b/src/lib/ai/__tests__/azure-token-budget.test.ts @@ -0,0 +1,207 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { resetJsonModeDialectCache } from "../json-dialect"; +import { OpenAIClient } from "../openai-client"; +import { + isAzureOpenAIV1Endpoint, + isMaxTokensRejection, + resetMaxTokensRefusals, + selectOpenAIChatCompletionsCapabilities, +} from "../openai-capabilities"; +import { resetReasoningDialectCache } from "../reasoning/dialect-cache"; +import { singleUserTurn } from "../types"; + +/** + * #1304 — an OpenAI-compatible gateway entry pointed straight at an Azure + * OpenAI v1 endpoint. A reasoning deployment refuses `max_tokens` with a 400; + * the request has to carry `max_completion_tokens` and no sampling controls, + * as it does on `api.openai.com`. Every other gateway keeps the legacy wire. + */ + +vi.mock("undici", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetch: (input: unknown, init?: unknown) => + (globalThis.fetch as unknown as (i: unknown, n?: unknown) => unknown)( + input, + init, + ), + }; +}); + +vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() })); + +const AZURE = "https://example-resource.openai.azure.com/openai/v1"; + +// The upstream error the report quotes, verbatim. +const AZURE_REFUSAL = JSON.stringify({ + error: { + message: + "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.", + type: "invalid_request_error", + param: "max_tokens", + code: "unsupported_parameter", + }, +}); + +function ok() { + return { + ok: true, + json: () => + Promise.resolve({ + choices: [ + { message: { content: '{"ok":true}' }, finish_reason: "stop" }, + ], + usage: { total_tokens: 5 }, + }), + }; +} + +function refused(status: number, body: string) { + return { ok: false, status, text: () => Promise.resolve(body) }; +} + +function gateway(model: string, baseUrl = AZURE) { + return new OpenAIClient({ + apiKey: "k", + model, + baseUrl, + providerType: "openai-compatible", + }); +} + +const turn = (responseFormat?: "json") => + singleUserTurn({ system: "s", user: "u", responseFormat }); + +function bodyOf(mock: ReturnType, call = 0) { + return JSON.parse(mock.mock.calls[call][1].body); +} + +beforeEach(() => { + vi.restoreAllMocks(); + resetJsonModeDialectCache(); + resetReasoningDialectCache(); + resetMaxTokensRefusals(); +}); + +describe("Azure OpenAI v1 endpoint", () => { + it.each([ + AZURE, + `${AZURE}/`, + "https://example-resource.cognitiveservices.azure.com/openai/v1", + "https://example-resource.services.ai.azure.com/openai/v1", + ])("recognises %s", (url) => { + expect(isAzureOpenAIV1Endpoint(url)).toBe(true); + }); + + it.each([ + "http://example-resource.openai.azure.com/openai/v1", + "https://example-resource.openai.azure.com.example.com/openai/v1", + "https://openai.azure.com/openai/v1", + "https://example-resource.openai.azure.com/openai/deployments/x", + "https://example-resource.openai.azure.com:8443/openai/v1", + `${AZURE}?api-version=preview`, + "https://litellm.example.com/v1", + "not a url", + ])("does not take %s for one", (url) => { + expect(isAzureOpenAIV1Endpoint(url)).toBe(false); + }); + + it("sends a reasoning deployment max_completion_tokens and no temperature", async () => { + const mock = vi.fn().mockResolvedValue(ok()); + vi.stubGlobal("fetch", mock); + await gateway("gpt-6-luna-example").generateCompletion(turn()); + expect(mock).toHaveBeenCalledTimes(1); + const body = bodyOf(mock); + expect(body.max_completion_tokens).toBeGreaterThan(0); + expect(body).not.toHaveProperty("max_tokens"); + expect(body).not.toHaveProperty("temperature"); + expect(body).not.toHaveProperty("seed"); + }); + + it("keeps the legacy wire for a GPT-4 deployment", async () => { + const mock = vi.fn().mockResolvedValue(ok()); + vi.stubGlobal("fetch", mock); + await gateway("gpt-4o").generateCompletion(turn()); + const body = bodyOf(mock); + expect(body.max_tokens).toBeGreaterThan(0); + expect(body.temperature).toBe(0.3); + expect(body).not.toHaveProperty("max_completion_tokens"); + }); + + it("keeps the legacy wire for a reasoning model name behind another gateway", () => { + expect( + selectOpenAIChatCompletionsCapabilities( + "https://litellm.example.com/v1", + "gpt-6-luna", + ).tokenBudgetField, + ).toBe("max_tokens"); + }); +}); + +describe("an endpoint that refuses max_tokens", () => { + it("asks again with max_completion_tokens for an opaque deployment name and remembers it", async () => { + const mock = vi + .fn() + .mockResolvedValueOnce(refused(400, AZURE_REFUSAL)) + .mockResolvedValue(ok()); + vi.stubGlobal("fetch", mock); + const client = gateway("prod-chat"); + + const result = await client.generateCompletion(turn()); + expect(result.content).toBe('{"ok":true}'); + expect(mock).toHaveBeenCalledTimes(2); + expect(bodyOf(mock, 0).max_tokens).toBeGreaterThan(0); + const repeat = bodyOf(mock, 1); + expect(repeat.max_completion_tokens).toBe(bodyOf(mock, 0).max_tokens); + expect(repeat).not.toHaveProperty("max_tokens"); + expect(repeat).not.toHaveProperty("temperature"); + + await client.generateCompletion(turn()); + expect(mock).toHaveBeenCalledTimes(3); + expect(bodyOf(mock, 2)).toHaveProperty("max_completion_tokens"); + }); + + it("learns it for that model only", async () => { + const mock = vi + .fn() + .mockResolvedValueOnce(refused(400, AZURE_REFUSAL)) + .mockResolvedValue(ok()); + vi.stubGlobal("fetch", mock); + await gateway("prod-chat").generateCompletion(turn()); + await gateway("prod-legacy").generateCompletion(turn()); + expect(bodyOf(mock, 2)).toHaveProperty("max_tokens"); + }); + + it("keeps JSON mode on the repeat", async () => { + const mock = vi + .fn() + .mockResolvedValueOnce(refused(400, AZURE_REFUSAL)) + .mockResolvedValue(ok()); + vi.stubGlobal("fetch", mock); + await gateway("prod-chat").generateCompletion(turn("json")); + expect(bodyOf(mock, 1).response_format).toEqual({ type: "json_object" }); + }); + + it("does not take a complaint about the value for a refusal of the field", async () => { + const mock = vi + .fn() + .mockResolvedValue( + refused(400, '{"error":"max_tokens is too large: 900000"}'), + ); + vi.stubGlobal("fetch", mock); + await expect( + gateway("prod-chat").generateCompletion(turn()), + ).rejects.toMatchObject({ httpStatus: 400 }); + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("reads only a 400 that names the field", () => { + expect(isMaxTokensRejection(400, AZURE_REFUSAL)).toBe(true); + expect(isMaxTokensRejection(422, AZURE_REFUSAL)).toBe(false); + expect( + isMaxTokensRejection(400, '{"error":"Unsupported parameter: top_k"}'), + ).toBe(false); + }); +}); diff --git a/src/lib/ai/json-dialect.ts b/src/lib/ai/json-dialect.ts index 0c1859105..5442707eb 100644 --- a/src/lib/ai/json-dialect.ts +++ b/src/lib/ai/json-dialect.ts @@ -57,8 +57,8 @@ export function resetJsonModeDialectCache(): void { * True when a 4xx error body reads as "this endpoint rejects the * `response_format` field" — either it names the field outright or it * complains about an unknown/unexpected parameter. A complaint that names - * `reasoning_effort` and not `response_format` is about the reasoning field, - * so it must not switch the endpoint's JSON mode off. + * `reasoning_effort` or `max_tokens` (#1304) and not `response_format` is + * about that field, so it must not switch the endpoint's JSON mode off. */ export function isResponseFormatRejection( status: number, @@ -66,7 +66,7 @@ export function isResponseFormatRejection( ): boolean { if (status < 400 || status >= 500) return false; if ( - /reasoning[\s_-]*effort/i.test(bodyExcerpt) && + /reasoning[\s_-]*effort|\bmax_tokens\b/i.test(bodyExcerpt) && !/response_format/i.test(bodyExcerpt) ) { return false; diff --git a/src/lib/ai/local-client.ts b/src/lib/ai/local-client.ts index 4fbe52d6b..bf97f551a 100644 --- a/src/lib/ai/local-client.ts +++ b/src/lib/ai/local-client.ts @@ -334,6 +334,8 @@ export class LocalOpenAICompatibleClient implements AIProvider { json: sent.json ? { endpoint: this.config.baseUrl, action: "ai.local.jsonDialect" } : null, + // Local runtimes take `max_tokens`; this wire has no other field. + budget: null, }); if ((lesson || sent.streamed) && hops.canSend) { return this.generateCompletion(params, hops); diff --git a/src/lib/ai/openai-capabilities.ts b/src/lib/ai/openai-capabilities.ts index 407b6c2e4..6848722ee 100644 --- a/src/lib/ai/openai-capabilities.ts +++ b/src/lib/ai/openai-capabilities.ts @@ -47,16 +47,86 @@ export function isCanonicalOpenAIEndpoint(baseUrl: string): boolean { } } +/** The hosts an Azure OpenAI resource is served from. */ +const AZURE_OPENAI_HOST = + /^[a-z0-9][a-z0-9-]*\.(openai\.azure\.com|cognitiveservices\.azure\.com|services\.ai\.azure\.com)$/; + +/** + * #1304 — an Azure OpenAI resource's v1 endpoint + * (`https://.openai.azure.com/openai/v1`). It speaks the same + * request contract as `api.openai.com` for the same model, but the model + * field carries the deployment name, which only identifies the model when + * the deployment was named after it. An opaque alias is caught by the + * refusal it draws instead (`rememberMaxTokensRefused`). + */ +export function isAzureOpenAIV1Endpoint(baseUrl: string): boolean { + try { + const url = new URL(baseUrl); + return ( + url.protocol === "https:" && + AZURE_OPENAI_HOST.test(url.hostname) && + url.port === "" && + url.pathname.replace(/\/+$/, "") === "/openai/v1" && + url.username === "" && + url.password === "" && + url.search === "" && + url.hash === "" + ); + } catch { + return false; + } +} + +/** + * #1304 — endpoint and model pairs that refused `max_tokens`, for the + * process lifetime. Keyed by both, because one gateway can front a legacy + * model and a reasoning model side by side. + */ +const maxTokensRefused = new Set(); + +function refusalKey(baseUrl: string, model: string): string { + return `${baseUrl}\n${model}`; +} + +export function rememberMaxTokensRefused(baseUrl: string, model: string): void { + maxTokensRefused.add(refusalKey(baseUrl, model)); +} + +/** Test hook — clears the learned `max_tokens` refusals. */ +export function resetMaxTokensRefusals(): void { + maxTokensRefused.clear(); +} + +/** + * True when a 400 says the model takes `max_completion_tokens` and not + * `max_tokens`. A complaint about the value ("max_tokens is too large") is + * not a refusal of the field and does not match. + */ +export function isMaxTokensRejection(status: number, body: string): boolean { + if (status !== 400 || !/\bmax_tokens\b/.test(body)) return false; + return ( + /max_completion_tokens/.test(body) || + /unsupported[\s_-]*parameter|not supported/i.test(body) + ); +} + /** * Select Chat Completions request capabilities from both endpoint and model. * Custom OpenAI-compatible gateways retain the broadly supported legacy wire, - * even when their model name resembles a modern OpenAI model. + * even when their model name resembles a modern OpenAI model, until the + * endpoint refuses `max_tokens` for that model. `api.openai.com` and an + * Azure OpenAI v1 endpoint take the modern wire for a reasoning model. */ export function selectOpenAIChatCompletionsCapabilities( baseUrl: string, model: string, ): OpenAIChatCompletionsCapabilities { - return isCanonicalOpenAIEndpoint(baseUrl) && isModernCanonicalModel(model) + if (maxTokensRefused.has(refusalKey(baseUrl, model))) { + return MODERN_CANONICAL_CAPABILITIES; + } + return (isCanonicalOpenAIEndpoint(baseUrl) || + isAzureOpenAIV1Endpoint(baseUrl)) && + isModernCanonicalModel(model) ? MODERN_CANONICAL_CAPABILITIES : LEGACY_COMPATIBLE_CAPABILITIES; } diff --git a/src/lib/ai/openai-client.ts b/src/lib/ai/openai-client.ts index 1e144ac2c..c4430706a 100644 --- a/src/lib/ai/openai-client.ts +++ b/src/lib/ai/openai-client.ts @@ -498,7 +498,8 @@ export class OpenAIClient implements AIProvider { * structured field (with anything key-shaped stripped), so an upstream * incident is diagnosable from the log instead of an opaque status. * - * An endpoint or model that refuses the reasoning field, or (#470, gateway + * An endpoint or model that refuses the reasoning field, `max_tokens` + * (#1304, asked again with `max_completion_tokens`), or (#470, gateway * only) the JSON flag, is asked once more without it and remembers * (`wire-refusal.ts`), inside the call's request budget. A refusal the * budget leaves no repeat for is marked `reasoningRefused`. @@ -534,6 +535,15 @@ export class OpenAIClient implements AIProvider { this.isGateway && sent.sendJsonFormat ? { endpoint: this.config.baseUrl, action: "ai.compat.jsonDialect" } : null, + // The request that failed carried `max_tokens`: nothing is learned + // between sending it and reading its refusal, so the selection holds. + budget: + selectOpenAIChatCompletionsCapabilities( + this.config.baseUrl, + this.config.model, + ).tokenBudgetField === "max_tokens" + ? { endpoint: this.config.baseUrl } + : null, }); if (lesson && hops.canSend) return this.generateCompletion(params, hops); const modelNotServed = await this.modelNoLongerServed(res.status); diff --git a/src/lib/ai/wire-refusal.ts b/src/lib/ai/wire-refusal.ts index 64136db6b..a1909722d 100644 --- a/src/lib/ai/wire-refusal.ts +++ b/src/lib/ai/wire-refusal.ts @@ -3,10 +3,12 @@ * clients, in one place for both. * * Only an HTTP 400 that names a field the request carried teaches anything: - * the reasoning field (`isReasoningParameterRejection`, learned per endpoint - * and model) or the JSON flag (`isResponseFormatRejection`, learned per - * endpoint). The reasoning field is checked first, so a body that names it - * never switches JSON mode off. Whether the call then sends the lesser request + * `max_tokens` where the model takes `max_completion_tokens` (#1304, + * `isMaxTokensRejection`, learned per endpoint and model), the reasoning + * field (`isReasoningParameterRejection`, learned per endpoint and model) or + * the JSON flag (`isResponseFormatRejection`, learned per endpoint). The + * JSON flag is checked last, so a body that names another field never + * switches JSON mode off. Whether the call then sends the lesser request * is the client's, inside its request budget (`request-hops.ts`). * * Server-only. @@ -17,13 +19,17 @@ import { isResponseFormatRejection, rememberJsonModeDialect, } from "./json-dialect"; +import { + isMaxTokensRejection, + rememberMaxTokensRefused, +} from "./openai-capabilities"; import { learnReasoningFieldRefused } from "./reasoning/answerless"; import { isReasoningParameterRejection, type ReasoningDialectKey, } from "./reasoning/dialect-cache"; -export type RefusalLesson = "reasoning" | "json" | null; +export type RefusalLesson = "budget" | "reasoning" | "json" | null; export function learnFromRefusal(args: { status: number; @@ -39,8 +45,18 @@ export function learnFromRefusal(args: { } | null; /** The endpoint, when the request carried the JSON flag. */ json: { endpoint: string; action: string } | null; + /** The endpoint, when the request carried `max_tokens` it could drop. */ + budget: { endpoint: string } | null; }): RefusalLesson { - const { reasoning, json } = args; + const { reasoning, json, budget } = args; + if (budget && isMaxTokensRejection(args.status, args.body)) { + rememberMaxTokensRefused(budget.endpoint, args.model); + annotate({ + action: { name: "ai.compat.tokenBudgetField" }, + meta: { field: "max_completion_tokens", httpStatus: args.status }, + }); + return "budget"; + } if ( reasoning && isReasoningParameterRejection( From 2676f2fce083c3da9cdcf5ffda26bef3f85e1826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 30 Oct 2026 17:09:01 +0100 Subject: [PATCH 2/4] chore(release): v1.46.5 --- CHANGELOG.md | 15 +++++++++++++++ docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c17f2723..0ef17b272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.46.5] — 2026-10-30 + +### Fixed + +- **Azure OpenAI reasoning deployments answer through the gateway.** An + OpenAI-compatible gateway pointed at an Azure OpenAI v1 endpoint sent + `max_tokens` and `temperature`, and a reasoning deployment (gpt-5 on, + the o-series) refused the request with a 400. A deployment named after + its model now gets `max_completion_tokens` and no sampling controls + from the first request, as on `api.openai.com`. A deployment with a + name of its own, or any other gateway that refuses `max_tokens` the + same way, is asked once more with `max_completion_tokens` and keeps + that for the endpoint and model. GPT-4 deployments and other gateways + keep the wire they had. Reported in #1304. + ## [1.46.4] — 2026-10-30 ### Fixed diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 6576bb9ab..202e7ec9d 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.46.4 + version: 1.46.5 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index 4885d2023..6368ffab5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.46.4", + "version": "1.46.5", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 12269f680..9b9502e0f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.46.4"; + /* @sw-version-fallback */ "v1.46.5"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` From 31ecb7590146aef09f7d0716d6949317c3d5aa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 30 Oct 2026 17:09:12 +0100 Subject: [PATCH 3/4] test(backup): golden fixture for v1.46.5 --- tests/fixtures/golden-backups/v1.46.5.json | 1 + .../golden-backups/v1.46.5.manifest.json | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/fixtures/golden-backups/v1.46.5.json create mode 100644 tests/fixtures/golden-backups/v1.46.5.manifest.json diff --git a/tests/fixtures/golden-backups/v1.46.5.json b/tests/fixtures/golden-backups/v1.46.5.json new file mode 100644 index 000000000..9d6ac80a7 --- /dev/null +++ b/tests/fixtures/golden-backups/v1.46.5.json @@ -0,0 +1 @@ +{"schemaVersion":"2","exportedAt":"2026-10-30T16:09:08.289Z","userId":"round-trip-owner","appSettings":null,"accountSettings":{"heightCm":null,"dateOfBirth":null,"gender":null,"hasDiabetes":false,"displayName":null,"fullName":null,"insurerName":null,"insuranceNumber":null,"insuranceNumberEncrypted":null,"insurerIkNumber":null,"lastReportPracticeName":null,"avatarBytes":null,"avatarContentType":null,"avatarUpdatedAt":null,"homeTimezone":null,"homeSince":null,"homeLocationEncrypted":null,"environmentAirQualityEnabled":true,"timezone":"Europe/Berlin","locale":null,"unitPreference":null,"glucoseUnit":null,"timeFormat":"AUTO","dateFormat":"AUTO","modulePreferencesJson":null,"thresholdsJson":null,"healthScoreConfigJson":null,"sourcePriorityJson":null,"dashboardWidgetsJson":null,"insightsLayoutJson":null,"medicationListLayoutJson":null,"moodTagLayoutJson":null,"documentsLayoutJson":null,"moduleListLayoutJson":null,"activityGoalsJson":null,"reportSelectionJson":null,"globalExcludedInjectionSites":[],"healthKitConfigJson":null,"notificationPrefs":null,"moodReminderEnabled":false,"aiProvider":null,"aiModel":null,"aiBaseUrl":null,"aiCompatBaseUrl":null,"aiCompatModel":null,"aiProviderChain":null,"aiResponseTimeoutSeconds":null,"useCentralCodex":false,"insightsPrivacyMode":"aggregated","insightsExcludeMetrics":[],"disableCoach":false,"aiUsageLevel":null,"coachPrefsJson":null,"documentsAutoAiRead":false,"labsLocalOcrEnabled":false,"storeWorkoutRoutes":false,"onboardingCompletedAt":null,"onboardingTourCompleted":false,"onboardingTourProgressJson":null,"disclaimerAcknowledgedAt":null,"disclaimerAcknowledgedVersion":null,"passkeyUpgradeNudgeDismissed":false},"measurements":[{"id":"cmvv63ctz002ri5ov1fno1ff7","type":"BLOOD_GLUCOSE","value":5.4,"valueMin":null,"valueMax":null,"unit":"mmol/L","measuredAt":"2026-07-03T06:45:00.000Z","source":"MANUAL","notes":null,"notesEncrypted":null,"externalId":null,"externalSourceVersion":null,"aggregationProvenance":null,"glucoseContext":"FASTING","sleepStage":null,"rhythmClassification":null,"deviceType":null,"ingestTokenId":null,"syncVersion":1,"deletedAt":null,"createdAt":"2026-10-30T16:09:08.279Z","updatedAt":"2026-10-30T16:09:08.279Z"},{"id":"cmvv63cu0002si5ovpsdmt1n4","type":"SLEEP_DURATION","value":95,"valueMin":null,"valueMax":null,"unit":"min","measuredAt":"2026-07-03T02:00:00.000Z","source":"APPLE_HEALTH","notes":null,"notesEncrypted":null,"externalId":"round-trip-deep-sleep","externalSourceVersion":null,"aggregationProvenance":null,"glucoseContext":null,"sleepStage":"DEEP","rhythmClassification":null,"deviceType":null,"ingestTokenId":null,"syncVersion":1,"deletedAt":null,"createdAt":"2026-10-30T16:09:08.280Z","updatedAt":"2026-10-30T16:09:08.280Z"},{"id":"cmvv63cr20001i5ov8d1bjiil","type":"PULSE","value":61,"valueMin":null,"valueMax":null,"unit":"bpm","measuredAt":"2026-07-02T06:30:00.000Z","source":"APPLE_HEALTH","notes":null,"notesEncrypted":null,"externalId":"round-trip-deleted-sample","externalSourceVersion":null,"aggregationProvenance":null,"glucoseContext":null,"sleepStage":null,"rhythmClassification":null,"deviceType":null,"ingestTokenId":null,"syncVersion":4,"deletedAt":"2026-10-20T16:09:07.000Z","createdAt":"2026-10-30T16:09:08.174Z","updatedAt":"2026-10-30T16:09:08.174Z"},{"id":"cmvv63cqz0000i5ovhzyg2sjz","type":"WEIGHT","value":74.2,"valueMin":null,"valueMax":null,"unit":"kg","measuredAt":"2026-07-01T07:00:00.000Z","source":"MANUAL","notes":null,"notesEncrypted":null,"externalId":null,"externalSourceVersion":null,"aggregationProvenance":null,"glucoseContext":null,"sleepStage":null,"rhythmClassification":null,"deviceType":null,"ingestTokenId":null,"syncVersion":1,"deletedAt":null,"createdAt":"2026-10-30T16:09:08.171Z","updatedAt":"2026-10-30T16:09:08.171Z"},{"id":"cmvv63cr30002i5ovjtizk643","type":"IRREGULAR_RHYTHM_NOTIFICATION","value":1,"valueMin":null,"valueMax":null,"unit":"event","measuredAt":"2026-06-15T14:22:00.000Z","source":"WITHINGS","notes":null,"notesEncrypted":null,"externalId":null,"externalSourceVersion":null,"aggregationProvenance":null,"glucoseContext":null,"sleepStage":null,"rhythmClassification":"IRREGULAR","deviceType":null,"ingestTokenId":null,"syncVersion":1,"deletedAt":null,"createdAt":"2026-10-30T16:09:08.175Z","updatedAt":"2026-10-30T16:09:08.175Z"}],"customMedicationCategories":[{"key":"custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10","label":null,"labelEncrypted":"djEuWlZlUjltQzFhMk5CUXZPQVN3V3JjcXlEL1Y2d3lSQzVZbzB4cHFoRjRPNGRxT09aaE9uR0hXckxZK1pJdjNrcWt3PT0=","sortOrder":2,"isActive":true,"createdAt":"2026-10-30T16:09:08.195Z","updatedAt":"2026-10-30T16:09:08.195Z"}],"medications":[{"id":"cmvv63crh0005i5ovl2urjmny","treatmentClass":"GENERIC","dosesPerUnit":null,"unitsPerDose":"1","notificationsEnabled":true,"pausedAt":null,"snoozedUntil":null,"startsOn":"2026-06-10T00:00:00.000Z","endsOn":"2026-06-16T00:00:00.000Z","oneShot":false,"asNeeded":false,"deliveryForm":"ORAL","trackInjectionSites":false,"allowedInjectionSites":[],"liveActivityEnabled":false,"criticalAlarmEnabled":false,"atcCode":null,"rxNormCode":null,"lowStockNotifiedAt":null,"lowStockNotifiedThresholdDays":null,"reorderLeadDays":null,"externalSource":null,"externalId":null,"updatedAt":"2026-10-30T16:09:08.189Z","categoryUpdatedAt":"2026-10-30T16:09:08.196Z","name":"Round-trip tablet","dose":"5 mg","active":true,"trackIntake":true,"autoMarkTaken":false,"autoMarkTakenSince":null,"category":"custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10","createdAt":"2026-10-30T16:09:08.189Z","schedules":[{"id":"cmvv63cri0006i5ovcp1twn8e","daysOfWeek":null,"timesOfDay":[],"reminderGraceMinutes":null,"rrule":null,"rollingIntervalDays":null,"scheduleType":"SCHEDULED","cyclicOnWeeks":null,"cyclicOffWeeks":null,"doseWindows":null,"unitsPerDose":null,"windowStart":"08:00","windowEnd":"09:00","label":"Morning","dose":null},{"id":"cmvv63cu3002ui5ovr38uagif","daysOfWeek":null,"timesOfDay":[],"reminderGraceMinutes":null,"rrule":null,"rollingIntervalDays":3,"scheduleType":"SCHEDULED","cyclicOnWeeks":null,"cyclicOffWeeks":null,"doseWindows":null,"unitsPerDose":null,"windowStart":"20:00","windowEnd":"21:00","label":"Every third evening","dose":null}],"sideEffects":[{"id":"cmvv63cro000bi5ovcsuvpo9q","notes":null,"notesEncrypted":"djEuWWhOZFBkQnkrUnUvY01iRzUrYkthRFBHaWVXWU82cXJLazhWbENISDlaOURGbVMvdTg2VXJiOWpaTHBaemcwbU5wanJRSm15Wno2dWovMElib1k4UitWdXpGa01hWlk9","createdAt":"2026-10-30T16:09:08.196Z","occurredAt":"2026-07-01T21:00:00.000Z","category":"GI","entry":"NAUSEA","severity":3}],"pauseEras":[{"id":"cmvv63crz000qi5ovxp6dt5d7","pausedAt":"2026-05-02T08:00:00.000Z","resumedAt":"2026-05-20T08:00:00.000Z","createdAt":"2026-10-30T16:09:08.207Z"},{"id":"cmvv63crz000ri5ovkr0q64qu","pausedAt":"2026-07-15T08:00:00.000Z","resumedAt":null,"createdAt":"2026-10-30T16:09:08.207Z"}],"ingredients":[{"id":"cmvv63cry000pi5ov6pmk5j5k","updatedAt":"2026-10-30T16:09:08.207Z","nutrient":"vitamin_d","amountPerUnit":25,"unit":"ug","enteredAmount":1000,"enteredUnit":"IU","source":"LABEL_AI","createdAt":"2026-10-30T16:09:08.207Z"}],"courses":[{"startsOn":"2026-03-01","endsOn":"2026-03-07","endedAt":null,"note":null,"noteEncrypted":"djEuTkxuZkFoRVpSWi9EYlRMNEZxb0dSTThkNTBXMm0yckFmQkQwZXVCNnlZaTB1RTYvNDlycjNNYz0=","updatedAt":"2026-10-30T16:09:08.189Z","createdAt":"2026-10-30T16:09:08.189Z"},{"startsOn":"2026-06-10","endsOn":"2026-06-16","endedAt":"2026-06-16T09:30:00.000Z","note":null,"noteEncrypted":null,"updatedAt":"2026-10-30T16:09:08.189Z","createdAt":"2026-10-30T16:09:08.189Z"}],"doseChanges":[{"id":"cmvv63crw000ki5ov9kdgb0m4","note":null,"noteEncrypted":"djEuNW5Na05YaWYya082eXBkRlJWb0pidVJQK3ErWTkxc1NwcEczaUl3ek50aGlTZHZJQmZLbjhCSFJQbUFLUTFMeVdLTWNlU0ErWDJ4M09DZGFlUT09","effectiveFrom":"2026-04-01T08:00:00.000Z","doseValue":5,"doseUnit":"mg","createdAt":"2026-10-30T16:09:08.204Z"},{"id":"cmvv63crw000li5ovan17o9g5","note":null,"noteEncrypted":null,"effectiveFrom":"2026-06-01T08:00:00.000Z","doseValue":10,"doseUnit":"mg","createdAt":"2026-10-30T16:09:08.204Z"}],"inventoryItems":[{"id":"cmvv63crt000gi5ovsimlvmu6","notes":null,"notesEncrypted":"djEud0hVTmVlNitPeWxQdU15MWRyaHRpSzM2NnNJc2swamE2UFJwMFYxVWF4S1JWbThIMjF3bW43WFFNYUFMV3FVeHVyU01jNTBPdUo3V0tnTjdlaWdnQUg3Rzh3PT0=","state":"ACTIVE","containerType":"BLISTER","unitsTotal":"30","unitsRemaining":"21","firstUseAt":"2026-07-01T08:00:00.000Z","expiresAt":"2027-01-31T00:00:00.000Z","printedExpiry":null,"purchasedAt":null,"manufacturer":"Fixture Pharma","doseStrength":"10 mg","createdAt":"2026-10-30T16:09:08.201Z","updatedAt":"2026-10-30T16:09:08.201Z"},{"id":"cmvv63crv000hi5ovmnuz4f9b","notes":null,"notesEncrypted":null,"state":"USED_UP","containerType":"BOTTLE","unitsTotal":"60","unitsRemaining":"0","firstUseAt":null,"expiresAt":null,"printedExpiry":null,"purchasedAt":null,"manufacturer":null,"doseStrength":null,"createdAt":"2026-10-30T16:09:08.203Z","updatedAt":"2026-10-30T16:09:08.203Z"}],"phaseConfig":{"id":"cmvv63crs000fi5ov73nwtumt","greenValue":45,"greenMode":"MINUTES","yellowValue":20,"yellowMode":"PERCENT","orangeValue":5,"orangeMode":"MINUTES","redValue":180,"redMode":"MINUTES"},"inventoryEvents":[{"id":"cmvv63crv000ii5ov7eqqxsmu","delta":30,"reason":"purchased","occurredAt":"2026-07-01T07:00:00.000Z"},{"id":"cmvv63crv000ji5ovmx2h160c","delta":-8,"reason":"consumed","occurredAt":"2026-07-09T07:00:00.000Z"}],"scheduleRevisions":[{"id":"cmvv63crx000mi5ovhv9794ss","validFrom":"2026-01-01T00:00:00.000Z","validUntil":"2026-03-01T00:00:00.000Z","payload":[{"dose":"5 mg","windowEnd":"09:00","windowStart":"07:00"}],"source":"ARCHIVED","supersededByIndex":null,"createdAt":"2026-10-30T16:09:08.205Z"},{"id":"cmvv63cry000oi5ovxlxtu4yf","validFrom":"2026-02-01T00:00:00.000Z","validUntil":"2026-03-01T00:00:00.000Z","payload":[{"dose":"10 mg","windowEnd":"13:00","windowStart":"12:00"}],"source":"ARCHIVED","supersededByIndex":2,"createdAt":"2026-10-30T16:09:08.206Z"},{"id":"cmvv63crx000ni5ovaha4sm0f","validFrom":"2026-03-01T00:00:00.000Z","validUntil":"2026-05-01T00:00:00.000Z","payload":[{"dose":"10 mg","windowEnd":"10:00","windowStart":"08:00"}],"source":"MANUAL","supersededByIndex":null,"createdAt":"2026-10-30T16:09:08.205Z"}],"efficacyTargets":[{"id":"cmvv63csb000zi5ovappynjxg","createdAt":"2026-10-30T16:09:08.219Z","updatedAt":"2026-10-30T16:09:08.219Z","measurementType":null,"biomarkerName":"Ferritin","primary":true}]}],"intakeEvents":[{"id":"cmvv63crl0009i5ovli3mxukd","medicationId":"cmvv63crh0005i5ovl2urjmny","autoMissed":false,"attributionSource":"AUTO","idempotencyKey":null,"createdAt":"2026-10-30T16:09:08.193Z","injectionSite":null,"doseTaken":null,"inventoryConsumption":null,"externalId":null,"updatedAt":"2026-10-30T16:09:08.193Z","syncVersion":0,"deletedAt":null,"medication":"Round-trip tablet","scheduledFor":"2026-07-01T08:00:00.000Z","takenAt":"2026-07-01T08:04:00.000Z","skipped":false,"source":"WEB","autoRecorded":false}],"moodEntries":[{"note":null,"noteEncrypted":null,"syncedAt":"2026-10-30T16:09:08.213Z","syncVersion":0,"createdAt":"2026-10-30T16:09:08.213Z","updatedAt":"2026-10-30T16:09:08.213Z","linkCreatedAt":[{"key":"round_trip_factor","createdAt":"2026-10-30T16:09:08.213Z"}],"id":"cmvv63cs5000ui5ovj87rgrie","date":"2026-07-01","mood":"GUT","score":4,"a1":null,"a2":null,"a3":null,"a4":null,"a5":null,"tags":null,"source":"MOODLOG","loggedAt":"2026-07-01T20:00:00.000Z","externalId":null,"tz":null,"deletedAt":null,"factors":[{"key":"round_trip_factor","rating":4}],"structuredTags":[],"context":{"notesEncrypted":null,"createdAt":"2026-10-30T16:09:08.213Z","updatedAt":"2026-10-30T16:09:08.213Z","workStatus":"regular","workMinutes":null,"overtimeMinutes":null,"workLoad":null,"workSatisfaction":null,"contactCircles":"[\"family\"]","contactForm":null,"contactExtent":null,"contactQuality":null,"contactSupport":null,"leisureCategories":null,"leisureMinutes":null,"leisureJoy":6,"leisureRecovery":null,"eventType":null,"eventValence":null,"eventAt":null}}],"customMoodTagCategories":[{"id":"cmvv63cs2000si5ovjjrrdxy5","key":"round_trip_own_category","labelKey":"mood.category.roundTripOwn","icon":null,"sortOrder":90,"isActive":true,"labelEncrypted":null}],"hiddenMoodTags":[{"key":"grateful","createdAt":"2026-10-30T16:09:08.212Z"}],"customMoodTags":[{"id":"cmvv63cs3000ti5ovl2vqhp9q","key":"round_trip_factor","labelKey":"mood.tag.roundTripFactor","categoryId":"cmvv63cs2000si5ovjjrrdxy5","kind":"RATED","isActive":true,"icon":null,"sortOrder":0,"labelEncrypted":null,"scaleMin":1,"scaleMax":5,"inverse":false}],"cycleProfile":{"id":"cmvv63csq001fi5ov155ygzd4","createdAt":"2026-10-30T16:09:08.234Z","updatedAt":"2026-10-30T16:09:08.234Z","goal":"GENERAL_HEALTH","cycleTrackingEnabled":true,"typicalCycleLength":null,"typicalPeriodLength":null,"lutealPhaseLength":null,"secondarySymptom":"MUCUS","predictionEnabled":true,"rawChartMode":false,"discreetNotifications":false,"sensitiveCategoryEncryption":true},"cycles":[{"id":"cmvv63csr001gi5ov94f5hl71","isPredicted":false,"syncVersion":0,"deletedAt":null,"absorbedIntoId":null,"createdAt":"2026-10-30T16:09:08.235Z","updatedAt":"2026-10-30T16:09:08.235Z","startDate":"2026-06-01","endDate":null,"periodEndDate":null,"lengthDays":null,"ovulationDate":null,"ovulationConfirmed":false,"tz":null},{"id":"cmvv63cu1002ti5ovsdesshcf","isPredicted":false,"syncVersion":0,"deletedAt":"2026-06-04T07:00:00.000Z","absorbedIntoId":"cmvv63csr001gi5ov94f5hl71","createdAt":"2026-10-30T16:09:08.281Z","updatedAt":"2026-10-30T16:09:08.281Z","startDate":"2026-06-03","endDate":null,"periodEndDate":null,"lengthDays":null,"ovulationDate":null,"ovulationConfirmed":false,"tz":null}],"cycleDayLogs":[{"id":"cmvv63csu001ii5ovuyl93pk2","cycleId":"cmvv63csr001gi5ov94f5hl71","syncVersion":0,"deletedAt":null,"createdAt":"2026-10-30T16:09:08.238Z","updatedAt":"2026-10-30T16:09:08.238Z","date":"2026-06-02","flow":"HEAVY","intermenstrualBleeding":false,"basalBodyTempC":null,"temperatureExcluded":false,"ovulationTest":null,"cervicalMucus":null,"cervixPosition":null,"cervixFirmness":null,"cervixOpening":null,"sexualActivity":false,"protectedSex":null,"pregnancyTest":null,"progesteroneTest":null,"contraceptive":null,"sensitiveEncrypted":null,"notesEncrypted":null,"source":"MANUAL","externalId":null,"tz":null,"symptomKeys":["custom:round-trip-cramps"],"symptomSeverities":[],"symptomLinkCreatedAt":[{"key":"custom:round-trip-cramps","createdAt":"2026-10-30T16:09:08.238Z"}]}],"customSymptoms":[{"id":"cmvv63css001hi5ov19mqnip7","key":"custom:round-trip-cramps","labelKey":"cycle.symptom.custom","categoryId":"csc_physical","icon":null,"sortOrder":0,"isActive":true,"labelEncrypted":"v1.2yNNoNbT3FZbQ8AHfllCmGtkSjsfrMOGvnT0QHsjlkq/pAsaWNQ2b9Aoq8QT"}],"labResults":[{"id":"cmvv63csa000yi5ov0b3oitmb","biomarkerId":"cmvv63cs8000vi5ov0uvczud7","noteEncrypted":null,"createdAt":"2026-10-30T16:09:08.218Z","updatedAt":"2026-10-30T16:09:08.218Z","deletedAt":null,"note":null,"panel":null,"analyte":"Ferritin","value":91,"valueText":null,"unit":"ng/mL","referenceLow":null,"referenceHigh":null,"sourceReferenceLow":null,"sourceReferenceHigh":null,"sourceReferenceText":null,"sourceValue":null,"sourceUnit":null,"takenAt":"2026-06-30T09:00:00.000Z","source":"MANUAL","biomarkerName":"Ferritin"}],"biomarkers":[{"id":"cmvv63cs8000vi5ov0uvczud7","createdAt":"2026-10-30T16:09:08.216Z","updatedAt":"2026-10-30T16:09:08.216Z","name":"Ferritin","unit":"ng/mL","lowerBound":null,"upperBound":null,"panel":null,"hidden":false,"context":null,"contextEncrypted":null,"analyteKey":null,"aliases":[{"name":"Ferr.","source":"IMPORT_CHOICE","id":"cmvv63cs9000wi5ovg1qxjchm","createdAt":"2026-10-30T16:09:08.217Z"}]}],"biomarkerMerges":[{"id":"cmvv63cs9000xi5ovaoayo6jq","targetBiomarkerId":"cmvv63cs8000vi5ov0uvczud7","targetName":"Ferritin","sourceBiomarkerId":"merged-away-marker","sourceName":"Ferritin (Serum)","readingCount":3,"mergedAt":"2026-10-30T16:09:08.217Z"}],"illnessEpisodes":[{"id":"cmvv63cse0012i5ov1p1u7q45","label":"Cold","type":"INFECTION","lifecycle":"ACUTE","onsetAt":"2026-06-20T00:00:00.000Z","resolvedAt":null,"parentConditionId":null,"note":null,"noteEncrypted":null,"bodySite":null,"bodySiteEncrypted":null,"laterality":null,"createdAt":"2026-10-30T16:09:08.222Z","updatedAt":"2026-10-30T16:09:08.222Z","deletedAt":null,"dayLogs":[{"id":"cmvv63csg0015i5ovowfxxadd","episodeId":"cmvv63cse0012i5ov1p1u7q45","date":"2026-06-21","functionalImpact":null,"feverC":null,"note":null,"noteEncrypted":null,"tz":null,"createdAt":"2026-10-30T16:09:08.224Z","updatedAt":"2026-10-30T16:09:08.224Z","deletedAt":null,"symptoms":[{"key":"body_aches","severity":2,"createdAt":"2026-10-30T16:09:08.224Z"}]}]},{"id":"cmvv63csf0013i5ovrue1ij1s","label":"Iron deficiency","type":"CHRONIC","lifecycle":"ACUTE","onsetAt":"2026-03-01T00:00:00.000Z","resolvedAt":null,"parentConditionId":null,"note":null,"noteEncrypted":null,"bodySite":null,"bodySiteEncrypted":"djEuVEFwQmJ3Mk1hQ2tOYVkwaFpqVFdIekN5YTE5cE5ybE82VXVHZUxYdGhRcmYxRUE9","laterality":"BOTH","createdAt":"2026-10-30T16:09:08.223Z","updatedAt":"2026-10-30T16:09:08.223Z","deletedAt":null,"dayLogs":[]}],"allergies":[{"id":"cmvv63csc0010i5ovfpm829ki","substance":"Penicillin","category":"OTHER","type":"ALLERGY","severity":null,"status":"ACTIVE","onsetAt":null,"reaction":null,"note":null,"reactionEncrypted":null,"notesEncrypted":null,"createdAt":"2026-10-30T16:09:08.220Z","updatedAt":"2026-10-30T16:09:08.220Z","deletedAt":null}],"familyHistory":[{"id":"cmvv63csd0011i5ovhsto2ec7","relationship":"MOTHER","condition":"Type 2 diabetes","ageAtOnset":null,"note":null,"notesEncrypted":null,"createdAt":"2026-10-30T16:09:08.221Z","updatedAt":"2026-10-30T16:09:08.221Z","deletedAt":null}],"workouts":[{"id":"cmvv63ct1001pi5ov5exa5utu","createdAt":"2026-10-30T16:09:08.245Z","updatedAt":"2026-10-30T16:09:08.245Z","externalSourceVersion":null,"sportType":"RUNNING","startedAt":"2026-06-29T06:00:00.000Z","endedAt":"2026-06-29T06:30:00.000Z","durationSec":1800,"totalEnergyKcal":null,"totalDistanceM":null,"avgHeartRate":null,"maxHeartRate":null,"minHeartRate":null,"stepCount":null,"elevationM":null,"pauseDurationSec":null,"source":"MANUAL","externalId":null}],"documents":[{"id":"cmvv63ct3001qi5ovamnyn8l9","kind":"LAB_RESULT","title":"June labs","filename":"june-labs.pdf","mimeType":"application/pdf","byteSize":57,"status":"STORED","reportDate":null,"documentDate":null,"sourceSystem":"PAPERLESS","sourceId":"4711","sourceInstance":null,"createdAt":"2026-10-30T16:09:08.247Z","updatedAt":"2026-10-30T16:09:08.247Z","contentEncrypted":"AgJ2MU+wwEXEmZPxSm7nI1Kpq/9bXMWCPlKTXL1Rtmlpzg9QjLtUJabYmI1DzoH3xHpVJy0LmCEO","contentSha256":null,"contentCodec":"binary2","providerType":null,"errorReason":null,"summaryEncrypted":null,"summaryGeneratedAt":null,"summaryState":"NONE","lastIndexAttemptAt":null,"lastIndexOutcome":null,"aiReadDeferred":true,"extractionIncomplete":false}],"manifest":{"documents":{"included":"encrypted-content","note":"Document metadata and encrypted stored bytes are included for disaster recovery."},"workouts":{"included":"summary-only","note":"Workout summary records (duration, distance, heart rate, calories, steps) are included. GPS routes and per-sample heart-rate/pace time series are not included in this export."},"mentalHealth":{"included":"full","note":"Completed screener administrations are included with their encrypted per-item answers, for restore onto the instance that holds the key."},"consent":{"included":"full","note":"Consent receipts are included, for restore onto the instance the consent was given to."}},"healthProfile":{"id":"cmvv63cso001di5oviquflul8","aboutMe":null,"conditions":null,"allergies":null,"coachFocus":null,"aiIncludedSections":["ABOUT_ME","CONDITIONS","ALLERGIES","COACH_FOCUS","FAMILY_HISTORY","SMOKING_STATUS","ALCOHOL_PATTERN","SHIFT_SCHEDULE"],"lifeConfirmedJson":null,"emergencyBloodType":null,"organDonorStatus":null,"advanceDirectiveStatus":null,"emergencyContacts":null,"emergencyImplants":null,"emergencyNote":null,"emergencyContactsEncrypted":null,"emergencyImplantsEncrypted":null,"emergencyNoteEncrypted":null,"aboutMeEncrypted":"djEucFRzYVc5dWpoVENhUmQyWWx3OWlocDAvaitzK0lFSUZsM0ZXRmNiOEdzc1JIdG5IdXFXNFh2OTRnTk5VV1NLQlhpSTY=","conditionsEncrypted":null,"allergiesEncrypted":null,"coachFocusEncrypted":null,"pendingQuestionsEncrypted":null,"createdAt":"2026-10-30T16:09:08.232Z","updatedAt":"2026-10-30T16:09:08.232Z"},"healthProfileFacts":[{"id":"cmvv63csp001ei5ovnmayb5pa","kind":"SMOKING_STATUS","value":null,"valueEncrypted":"djEuUStrOFFCMWtWeWp3Sll6YytMVlpaTmF3Ymc0ODRBaUkwMTNtRjlUYmtVNkc=","validFrom":"2026-01-01T00:00:00.000Z","validUntil":null,"provenance":"USER_REPORTED","supersededByRevisionId":null,"createdAt":"2026-10-30T16:09:08.233Z"}],"customMetrics":[{"id":"cmvv63csx001ji5ov9w3zz4g1","createdAt":"2026-10-30T16:09:08.241Z","updatedAt":"2026-10-30T16:09:08.241Z","deletedAt":null,"name":"Grip strength","unit":"kg","targetLow":null,"targetHigh":null,"decimals":null,"description":null,"correlationEnabled":false,"entries":[{"id":"cmvv63csx001ki5ov8xxxmvdo","createdAt":"2026-10-30T16:09:08.241Z","deletedAt":null,"noteEncrypted":null,"value":44,"unit":"kg","measuredAt":"2026-07-01T18:00:00.000Z","note":"left hand, after the long ride"},{"id":"cmvv63csx001li5ovz9n1gdng","createdAt":"2026-10-30T16:09:08.241Z","deletedAt":null,"noteEncrypted":"djEuaUZHS2NPOUdTSlArZjRuT253emE3aDh1Q2cxWU1QV1NIVUxPdWlpYktIMHVDMUJjTFdHTXNIRHkxQ1Z5MkE9PQ==","value":45,"unit":"kg","measuredAt":"2026-07-02T18:00:00.000Z","note":null}]}],"correlationPatterns":[{"id":"cmvv63csz001mi5ov18hg4evl","updatedAt":"2026-10-30T16:09:08.243Z","createdAt":"2026-10-30T16:09:08.243Z","canonicalKey":"p1:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1","family":"sleep","factorKey":"sleep_duration","outcomeKey":"weight","lagDays":1,"sampleSize":30,"effectSize":0.42,"pValue":0.01,"qValue":null,"evidenceHash":"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2","isCurrent":true,"lastComputedAt":"2026-07-01T00:00:00.000Z","dismissedAt":null,"dismissedEvidenceHash":null,"dismissedEffectSize":null,"dismissedSampleSize":null}],"decryptFailures":[],"intradayProfiles":[{"id":"cmvv63cr70004i5ovz5snoqnv","createdAt":"2026-10-30T16:09:08.179Z","updatedAt":"2026-10-30T16:09:08.179Z","type":"ACTIVITY_STEPS","dateKey":"2026-07-01","hourlyCumulative":[0,350,700,1050,1400,1750,2100,2450,2800,3150,3500,3850,4200,4550,4900,5250,5600,5950,6300,6650,7000,7350,7700,8050],"dayTotal":8050,"sampleCount":24,"hourlyIncrements":[],"basis":"PER_SAMPLE","bucketCount":null,"timezone":"Europe/Berlin"}],"healthScoreRecords":[{"id":"cmvv63ct0001oi5ovxyhmur70","createdAt":"2026-10-30T16:09:08.244Z","dayKey":"2026-07-01","timezone":"Europe/Berlin","composite":71,"band":"green","scoreVersion":1,"composition":["activity","sleep"],"pillarScores":{"sleep":72,"activity":70},"inputFingerprint":"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3","configVersion":null,"configChangedAt":null,"computedAt":"2026-10-30T16:09:08.244Z"}],"onboardingRecord":{"id":"cmvv63ct0001ni5ovgcfjuy6r","createdAt":"2026-10-30T16:09:08.244Z","updatedAt":"2026-10-30T16:09:08.244Z","needs":{"recordTarget":"me","areas":["blood-pressure","sleep"],"medication":"yes","sources":["withings"],"visit":"within-a-month","units":{"glucoseUnit":"mmol/L","unitPreference":"metric"}},"steps":[{"id":"who","status":"done"},{"id":"areas","status":"done"},{"id":"medication","status":"pending"},{"id":"sources","status":"skipped"},{"id":"visit","status":"pending"},{"id":"units","status":"pending"},{"id":"confirm","status":"pending"},{"id":"first-result","status":"pending"},{"id":"done","status":"pending"}],"firstResult":{"task":"connect-source","target":"withings","completedAt":"2026-07-01T08:30:00.000Z"},"modulesDerivedAt":"2026-07-01T08:00:00.000Z","completedAt":"2026-07-01T08:00:00.000Z"},"practitioners":[{"id":"cmvv63ctg0022i5ovf9wghqwp","name":"Round-trip practice","specialty":"Internal medicine","practice":null,"location":null,"phone":null,"locationEncrypted":null,"phoneEncrypted":null,"noteEncrypted":"djEuZnY5UU5uUUlUL0xudTNxT0NPbmluUFNDNUs1Y3d0VlFNNk5FR0NjaUVyYmg3UnVtNEx5VEpUb1JFM051aXNZPQ==","createdAt":"2026-10-30T16:09:08.260Z","updatedAt":"2026-10-30T16:09:08.260Z","deletedAt":null}],"encounters":[{"id":"cmvv63ctk0026i5ov46w28i9w","occurredAt":"2026-06-30T08:00:00.000Z","status":"DONE","kind":"ROUTINE","practitionerId":"cmvv63ctg0022i5ovf9wghqwp","reasonEncrypted":"djEuZDkwWnJpNWhKKzB5UUEzWXVvM1psR1pFSUF4b3o1NllaTFoySTIyZHhBbWFpZ3pPVFJOcnpnPT0=","outcomeEncrypted":"djEuRUxkS1M0NDE5U2RVS3drUlI1WExRSFRhQk1YMXQ3NDdLOUF1OXc2ZlZ6OXpHZXVoWkVTTHlnbU8vZ3RxNUNJb29rbnVPQ0VYVkNCbW14VUR1aVhia0dUZU1MNE0=","bodySiteEncrypted":null,"laterality":null,"reminderId":"cmvv63cth0023i5ovgx0mamri","createdAt":"2026-10-30T16:09:08.264Z","updatedAt":"2026-10-30T16:09:08.264Z","deletedAt":null}],"encounterDocumentLinks":[{"encounterId":"cmvv63ctk0026i5ov46w28i9w","targetId":"cmvv63ct3001qi5ovamnyn8l9","createdAt":"2026-10-30T16:09:08.265Z"}],"encounterLabLinks":[{"encounterId":"cmvv63ctk0026i5ov46w28i9w","targetId":"cmvv63csa000yi5ov0b3oitmb","createdAt":"2026-10-30T16:09:08.265Z"}],"encounterConditionLinks":[{"encounterId":"cmvv63ctk0026i5ov46w28i9w","targetId":"cmvv63cse0012i5ov1p1u7q45","createdAt":"2026-10-30T16:09:08.266Z"}],"customVaccines":[{"id":"cmvv63ctn002ai5ovsbfadzgu","name":"Round-trip travel combo","components":["typhoid","hepatitis-a"],"typicalSeriesDoses":2,"boosterIntervalMonths":36,"createdAt":"2026-10-30T16:09:08.267Z","updatedAt":"2026-10-30T16:09:08.267Z","deletedAt":null}],"vaccinations":[{"id":"cmvv63cto002bi5ov33yw9t32","occurredAt":"2026-05-14T00:00:00.000Z","antigenSlug":"tdap","vaccineName":"Tetanus, diphtheria and pertussis","doseNumber":3,"seriesDoses":3,"lotNumber":"RT-4471","site":"LEFT_ARM","practitionerId":"cmvv63ctg0022i5ovf9wghqwp","encounterId":"cmvv63ctk0026i5ov46w28i9w","reminderId":"cmvv63cth0023i5ovgx0mamri","customVaccineId":"cmvv63ctn002ai5ovsbfadzgu","noteEncrypted":"djEuZnhtUm5oR2JTWkNiTEhWNnRKWThJOWZLTTgxc2xjRVFUK1FLOWxydU0rV0t4SjNWTi9USE5xWm5PQzNDYWVpT2ZqNE5oMlB4ODRFbU1oc1E=","createdAt":"2026-10-30T16:09:08.268Z","updatedAt":"2026-10-30T16:09:08.268Z","deletedAt":null}],"vaccinationDocumentLinks":[{"vaccinationId":"cmvv63cto002bi5ov33yw9t32","targetId":"cmvv63ct3001qi5ovamnyn8l9","createdAt":"2026-10-30T16:09:08.269Z"}],"measurementReminders":[{"id":"cmvv63cth0023i5ovgx0mamri","label":"Blutdruck messen","measurementType":"BLOOD_PRESSURE_SYS","intervalDays":7,"rrule":null,"anchorDate":null,"endsOn":null,"origin":"VORSORGE","notifyHour":8,"location":"Zuhause","nextDueAt":"2026-07-08T06:00:00.000Z","lastSatisfiedAt":"2026-07-01T06:05:00.000Z","enabled":true,"vaccinationAntigen":"tetanus","snoozedUntil":"2026-07-08T06:00:00.000Z","lastSkippedAt":"2026-06-24T06:00:00.000Z","skipCount":2,"createdAt":"2026-10-30T16:09:08.261Z","updatedAt":"2026-10-30T16:09:08.261Z","deletedAt":null}],"measurementReminderEvents":[{"id":"cmvv63ctj0025i5ovhdp9l1hm","reminderId":"cmvv63cth0023i5ovgx0mamri","kind":"SKIPPED","occurredAt":"2026-06-24T06:00:00.000Z","onTime":false,"source":"skip","createdAt":"2026-10-30T16:09:08.263Z"},{"id":"cmvv63cti0024i5ovklaozj9z","reminderId":"cmvv63cth0023i5ovgx0mamri","kind":"SATISFIED","occurredAt":"2026-07-01T06:05:00.000Z","onTime":true,"source":"manual","createdAt":"2026-10-30T16:09:08.262Z"}],"coachConversations":[{"id":"cmvv63ct8001ui5ovo5gt1oxl","title":"How is my blood pressure trending?","documentScoped":false,"summaryEncrypted":"djEuSWhucVNNN2hNdWJtRkowNlkxTjNFb0FyR0d0V0dzSDJKVDJaU3VxSkQybndsV2JuUnhPYWRndnVmcWNPa2NoaW95cFh1UlJkcXlqeVN5RlVzRVBlQXo5eU5TQ2xzVi93UlE9PQ==","summaryUpdatedAt":"2026-07-20T10:00:00.000Z","summaryTurnCount":4,"createdAt":"2026-10-30T16:09:08.252Z","updatedAt":"2026-10-30T16:09:08.252Z","messages":[{"id":"cmvv63ct8001vi5ovpqk7qaii","role":"user","contentEncrypted":"djEuZkg4NjJYKzVvc0wyNXhWQkU5N1NBQTdGdTBleC9RdkczQjJJWjJNNkhPQ1RRaWROc0pyV0U0K3pYMS9xS0tzeWh1WlN5cjNvZzBlTlU5Zm9YTnNQdXgzQXBOL21XU1FrNzlLb0xRPT0=","metricSourceJson":null,"providerType":null,"promptVersion":null,"tokensUsed":null,"model":null,"resultsEncrypted":null,"trailEncrypted":null,"createdAt":"2026-07-20T09:58:00.000Z"},{"id":"cmvv63ct8001wi5ov60r9bfsa","role":"assistant","contentEncrypted":"djEuZTVwWVI1U3FXRUs2MzVMdzU5WTNCazFHbjI0c0xUbVNHckEzMXNDTHhzanlMUXF6cElsaEJqeUliVGdsVVc5K3pCN00xNG5PcXFYMEpWZXZPZ3RMa212NWdURkFoVStzTmVwL3JvNWRoM216eXZ2WVk0SjZ6MnJZeDNyUA==","metricSourceJson":null,"providerType":"anthropic","promptVersion":null,"tokensUsed":812,"model":"claude-opus-5","resultsEncrypted":"djEuaVc1OWFYL3BFUUo4NUJsZWxWeFE0bGJHQ0N1a08veTVla3JsRnNTbEMwZXNrZjlTOGlhVXFhK1JKN0lSdUY3Tm1yZmF2eDJEYmxxL3EyUm5BK0k1OW56ZWt1K1FybkhWaCtENGZ4dU5VWDlhdUI3TnRic25ub3VDQVlzTW5abWNjVngvaVBmKzUwZVRlck5KN0xuUDJhTVJIbkhFTVBHVUNOWkRwWjRSZlpQc1lhUVFqY3FIbFFTMkwycXliMC9nbTgwVWc3eGlVeVhKQXJaRWQzLzNwRXJCZ0ZnQTBOS1hqc0ptb01idmdUL0xTSmdYS1RPUzQ2VFZnRURhMmtpdlB0SERubUlRL3V6V3JlS1FOTWo2bHl1ZXA2bml3Si9QV1ZhZ3BYcGZaSHREYVRCVExmVGQ3QVNSYXFySVhMTEV0YkorU2xmWEVYV0h3UFA2Y2ExWVd1dVFuTnFJbGFTN3gvVWNsOHNaKzJUOVNvUTBVWmowZkV0bVRpZ0hwTjVoWGtBaUFackdvWlB5dDd6UEg2RGkxdldHOVNZeUhTclBqYWwxdzk1RC8zcGJYR0VrRmd2ZWxETzh3MkpLQUo0QmM5ekxJMU1zb3Z4d0w5SHZpNnEvTXlNcVkyamFJTElENTk0Q0RLN2ZDZUNtdzNqV2hpSU9xc0JUcGJuY0VYQkpzSlZEMHlGakJ1dENQalFKWGNFclJDOVM4dHloMU8zUDhuK1pnUVdUSDRKVVArNTEyNWVUNU1lV1BPNVlQcGI0QkYyS2RtejNqaEdKaW11b2xYRENGZVN3Q0Z3MEgxWGNHeEMxNW1XdFRKRUJodjRGaUNya1Qxa2k0eFF2ck5mTGJPUFJrbmZJY0MzZ3NvMnYvdEozdis2OUJmMkNHc0dwakl2Y2d5bTEzclM5M2gwaklaWFd2bW5uVElsckN2RGdJSVZndnZ1dWpOcjUrczRiYm91eWJhemF6SW1VVVRySkhYUnZzUGk4bEEzU0c3ZVlTSUk0bTF2WW5qWGxuRVRpRXZwcnVNblpTdHdVZVpzL2NOc0Qva1VON0tuVGpTaWwxUW44Qkx5akVjb1E0Ni9FMFFKdjZVWHFUWGNmejJwbg==","trailEncrypted":"djEuTEVkVkdrclNTdTZCSVBmT3R5M0xIVzYvbTFNcmtwOGd5aVR0cXJIdnRsUlhLNFBKeUEySCtJZWJuWnlUMzVGcURWZmRpMGI3ajJEZ1l3ektxM1B5VmxvZy9jVy9RVExYVkloMVhWOC95VVhZaXFTWFRjbnhHaExkWndBVkRuSTQwQ1RvNGFJQ09MUjc=","createdAt":"2026-07-20T09:59:00.000Z"}],"attachments":[]},{"id":"cmvv63cta001xi5ovyvjcml9n","titleEncrypted":"djEuUnFMa0tJbE11YkpyWGJKTnRReTY4Yk5XU09RRTh1bkFCd3dBQmdvdjFlZW9iMFF5OURqc2k4KzFlTVIwbmc9PQ==","documentScoped":true,"summaryEncrypted":null,"summaryUpdatedAt":null,"summaryTurnCount":0,"createdAt":"2026-10-30T16:09:08.254Z","updatedAt":"2026-10-30T16:09:08.254Z","messages":[{"id":"cmvv63ctb001yi5ovkhn0b74e","role":"user","contentEncrypted":"djEuQk5lR1NHZzlTWmc1b0loTlJUQXZmTm44WUJzVks2dy9CaEdGbXJHQW9WT0cwZ2VkZDVIUDR6V2s1djBuaG9mRDVuQmJKR1Q4bzNFPQ==","metricSourceJson":null,"providerType":null,"promptVersion":null,"tokensUsed":null,"model":null,"resultsEncrypted":null,"trailEncrypted":null,"createdAt":"2026-07-21T08:00:00.000Z"}],"attachments":[{"documentId":"cmvv63ct3001qi5ovamnyn8l9","addedAt":"2026-07-21T07:59:00.000Z"}]}],"coachFacts":[{"id":"cmvv63cte001zi5ovosqkdwrc","factEncrypted":"djEuUm8rTVl2UURFbGpUT1YrdzF0a0xpUUNiV2hZUy9kaE54QVVoelVOUUtSQXNMZ3JuRFRqZ0ROWkxKbG16aitMYzROMGo0em9hQ1d5UFBqd3Q=","deletedAt":null,"category":"constraint","confidence":80,"sourceConversationId":"cmvv63ct8001ui5ovo5gt1oxl","source":"user","lastUsedAt":"2026-07-20T10:00:00.000Z","sourceMessageId":"cmvv63ct8001wi5ov60r9bfsa","createdAt":"2026-10-30T16:09:08.258Z","updatedAt":"2026-10-30T16:09:08.258Z"}],"coachPlans":[{"id":"cmvv63ctf0020i5ov4fqdkg9h","metric":"BLOOD_PRESSURE","ifCueEncrypted":"djEuZnlpM1hRK2Zjekx3VWdFL3A4MkZXLzA0eC9UTVk4WWFNZDRUa1p2OFhna25VN3NPMVBkeldMeXByUFA5Zy90bmlnanp5eTVlWU1GR3dwQndHdHM9","thenActionEncrypted":"djEudTVNOUI5d2lnZHZmMFN0eVZRSndEQklVTWhtZDhoamVwSU1Sdk8yWVdMMklsY05iRksrelNyMU1BVDQwTy9NME5uVm50b1pzSnhoVlBnSGRaK1l6OTBFNUNWU1M0MUx5emUycg==","targetEncrypted":"djEuNGlVdVE4QUswZHllRzZsbzNudWZEM2pzZkgrSlp6UFJkdHlFR0dJRmgzaXF1VnM5cXVORnlsUk1mU2hCZWZUbQ==","outcomeEncrypted":null,"deletedAt":null,"status":"active","reviewDate":"2026-10-01T00:00:00.000Z","sourceConversationId":"cmvv63ct8001ui5ovo5gt1oxl","createdAt":"2026-10-30T16:09:08.259Z","updatedAt":"2026-10-30T16:09:08.259Z"}],"coachReminders":[{"id":"cmvv63ctf0021i5ov9j6ro6b0","noteEncrypted":"djEuWXZpWllqbGdVQVZiWlE4cG0vcGd3U2FCSDRlbXBVL0diUytkZGc5QjJFRjZtSWRlcWVBQmI5U010NHVmanZPVW5PRW96aDBmTWlFbmZhc05HazVJ","deletedAt":null,"metric":"BLOOD_PRESSURE","relatedPlanId":"cmvv63ctf0020i5ov4fqdkg9h","triggerKind":"date","dueAt":"2026-09-01T07:00:00.000Z","contextCue":null,"status":"active","source":"extractor","sourceConversationId":"cmvv63ct8001ui5ovo5gt1oxl","lastSurfacedAt":null,"surfaceCount":2,"createdAt":"2026-10-30T16:09:08.259Z","updatedAt":"2026-10-30T16:09:08.259Z"}],"mentalHealthAssessments":[{"id":"cmvv63crq000ci5ov6pm4g950","instrument":"PHQ9","locale":"de","wording":null,"version":"standard","responsesEncrypted":"djEuanVJMFZQcTFsTDc1S2hFdmdiYWZsVXJJUm1jS0VNd3lVUXN3cjBUcGEvTDk2emVyNUx1UGVnL3NDQ1JmbFFGOUpMRWlwL1FCY2tOelc2enk3MGFPSTV0WjVlTT0=","totalScore":14,"severityBand":"moderate","item9Flagged":true,"crisisShownAt":"2026-07-05T18:02:00.000Z","takenAt":"2026-07-05T18:00:00.000Z","tz":"Europe/Berlin","source":"WEB","externalId":null,"syncVersion":0,"deletedAt":null,"createdAt":"2026-10-30T16:09:08.198Z","updatedAt":"2026-10-30T16:09:08.198Z"}],"consentReceipts":[{"id":"cmvv63crr000di5ovsh9qwyqb","kind":"ai-processing","artefact":"JVBERi0xLjQKJWZpeHR1cmU=","signedAt":"2026-06-01T09:00:00.000Z","revokedAt":null,"createdAt":"2026-10-30T16:09:08.199Z"},{"id":"cmvv63crr000ei5ov46w14vwi","kind":"research-sharing","artefact":"eyJhbGciOiJub25lIn0.e30.","signedAt":"2026-06-02T09:00:00.000Z","revokedAt":"2026-07-10T11:00:00.000Z","createdAt":"2026-10-30T16:09:08.199Z"}],"documentConditionLinks":[{"documentId":"cmvv63ct3001qi5ovamnyn8l9","episodeId":"cmvv63csf0013i5ovrue1ij1s","createdAt":"2026-07-02T09:00:00.000Z"}],"extractedFacts":[{"id":"cmvv63ct6001ti5ov55brtkqw","documentId":"cmvv63ct3001qi5ovamnyn8l9","factType":"OBSERVATION","status":"APPROVED","confidence":0.94,"needsReview":false,"committedRecordId":"cmvv63csa000yi5ov0b3oitmb","committedRecordType":"labResult","dataEncrypted":"djEuS0RUNmpJajV3YzIwczBDUGxxRGFPTnJMdGU5YThESnBrM2Z0WURLVWU0N1BaNTVTRzRkVldxZEQvK3pvVjZXa0oxV3Z6TlhINDU3UjZsTUFjMGdpY0Y1VjgxSlVXdStjcGMrVzh4NlFvOVdpTGI2elc0UW1yelNEZ3pZMkVsQ29pb3JiZ0lmYytjNWM1Vkk0SlErWXNWeW1oUW96elFYN3IvU3JqekpIaVYwV0RSVmZiM0h3NzRGUERucDlyMStJUU55SWpUaTJrRFlJVU55MEpFbk1uQWJIRlJ4ZXpTdUo4d3ROeTFjZUFYSWFrU3dDblhJVGdFVFFNZEU9","provenanceEncrypted":"djEuV0YvWXZwMy80T0pCUjg2NG9yczJ5eTlyTGdMRm5mdC9VYzgvaFBXQXlDVldDRDhuOFI3VXNLOTZ1ZkJHa3hrbnFrM3hQU0JGeUNDTUV3WW02U0ErTHZyWUxDU2tGcDFBcGpQc0xxemk3R2o5MnVReVJOU2s0VjJTYmxLWHBLYXFjRkZUOVc5YUp6ejF1S3Y2WkUzUHVqcWF0Tm5Pbkh3eVh3emdlVURueHBNc0xtOHdVUGVxYVNYMA==","createdAt":"2026-10-30T16:09:08.250Z","updatedAt":"2026-10-30T16:09:08.250Z"}],"documentImportKeys":[{"sourceSystem":"PAPRA","sourceId":"doc_deleted","sourceInstance":"https://papra.example","createdAt":"2026-07-03T09:00:00.000Z"}],"personalRecords":[{"metricType":"WALKING_RUNNING_DISTANCE","metricSlot":"running_5km_time","direction":"MIN","value":1512,"unit":"s","achievedAt":"2026-05-18T09:20:00.000Z","sourceMeasurementId":null,"source":"APPLE_HEALTH","externalId":"workout-pr-5km","createdAt":"2026-10-30T16:09:08.275Z"},{"metricType":"WALKING_RUNNING_DISTANCE","metricSlot":"running_10km_time","direction":"MIN","value":3184,"unit":"s","achievedAt":"2026-05-18T09:20:00.000Z","sourceMeasurementId":null,"source":"APPLE_HEALTH","externalId":"workout-pr-10km","createdAt":"2026-10-30T16:09:08.275Z"},{"metricType":"WEIGHT","metricSlot":null,"direction":"MIN","value":74.2,"unit":"kg","achievedAt":"2026-07-01T07:00:00.000Z","sourceMeasurementId":"cmvv63cqz0000i5ovhzyg2sjz","source":"MANUAL","externalId":null,"createdAt":"2026-10-30T16:09:08.274Z"}],"userAchievements":[{"achievementId":"retired-badge-from-an-older-release","unlockedAt":"2025-11-05T08:00:00.000Z","createdAt":"2026-10-30T16:09:08.275Z"},{"achievementId":"intake-total-10","unlockedAt":"2026-02-21T10:15:00.000Z","createdAt":"2026-10-30T16:09:08.275Z"}],"environmentContexts":[{"date":"2026-06-15","lat":null,"lon":null,"locationEncrypted":"AgJ2MaM8/WMw56GUwHqwQg/5kc5hvmTkEvpMwpBuUVjf5f8xYsAmR7JTmObshtjrXEMGvz1OYWbV2igRTEHZvJiEPGeKZNxoRP0=","locationLabel":null,"source":"TRAVEL","tempMin":19.4,"tempMax":28.1,"tempMean":23.6,"apparentMean":25.2,"sunshineSec":39600,"daylightSec":52800,"precipSum":0,"pressureMean":1016.4,"pressureDelta":3.2,"humidityMean":63,"cloudMean":12,"weatherCode":1,"apparentMax":null,"pm25Mean":null,"pm25Max":null,"pm10Mean":null,"no2Mean":null,"so2Mean":null,"coMean":null,"o3Max8h":null,"eaqiMax":null,"usaqiMax":null,"uvIndexMax":null,"dustMax":null,"aodMax":null,"pollenAlderMax":null,"pollenBirchMax":null,"pollenGrassMax":null,"pollenMugwortMax":null,"pollenOliveMax":null,"pollenRagweedMax":null,"aqDomain":null,"aqHours":null,"aqFetchedAt":null,"fetchedAt":"2026-06-16T03:15:00.000Z","createdAt":"2026-10-30T16:09:08.278Z","updatedAt":"2026-10-30T16:09:08.278Z"},{"date":"2026-07-01","lat":null,"lon":null,"locationEncrypted":"AgJ2MduLfQ0RjJyW5yLTJ3wzUtcFBqbsdklv9KAb12r6pH6HoOW5MzQ4pEUN113LMLkh5Z1Jx5GLs20dVMkDubUslpNrg5ob","locationLabel":null,"source":"HOME","tempMin":13.1,"tempMax":22.7,"tempMean":17.9,"apparentMean":17.1,"sunshineSec":28800,"daylightSec":59400,"precipSum":4.6,"pressureMean":1008.9,"pressureDelta":7.8,"humidityMean":74,"cloudMean":68,"weatherCode":61,"apparentMax":null,"pm25Mean":null,"pm25Max":null,"pm10Mean":null,"no2Mean":null,"so2Mean":null,"coMean":null,"o3Max8h":null,"eaqiMax":null,"usaqiMax":null,"uvIndexMax":null,"dustMax":null,"aodMax":null,"pollenAlderMax":null,"pollenBirchMax":null,"pollenGrassMax":null,"pollenMugwortMax":null,"pollenOliveMax":null,"pollenRagweedMax":null,"aqDomain":null,"aqHours":null,"aqFetchedAt":null,"fetchedAt":"2026-07-02T03:15:00.000Z","createdAt":"2026-10-30T16:09:08.278Z","updatedAt":"2026-10-30T16:09:08.278Z"}],"environmentTravelLocations":[{"startDate":"2026-06-10","endDate":"2026-06-20","lat":null,"lon":null,"label":null,"locationEncrypted":"AgJ2MQNya6vvGb3hDhJubxVOQRKspamAMfOFIRE1c6d95J8Pn/OgFDPtQekgTAq6gkRAEpdsNvvbSumy/NxfnrFZJKH1en1mJnk=","createdAt":"2026-10-30T16:09:08.277Z","updatedAt":"2026-10-30T16:09:08.277Z"}],"dawarichDays":[{"date":"2026-06-15","totalDistanceM":48300,"drivingDistanceM":12000,"trainDistanceM":30500,"placesVisited":4,"awayFromHomeMinutes":1440,"countryCode":null,"countryCodeEncrypted":"AgJ2MSuhQLJ9bnS3Zg/4JphjBlAbHWEZ3UDdPRqgA1paow==","isTravelDay":true,"fetchedAt":"2026-06-16T03:20:00.000Z","createdAt":"2026-10-30T16:09:08.276Z","updatedAt":"2026-10-30T16:09:08.276Z"}],"ecgRecordings":[{"id":"cmvv63cr40003i5ovt9dtdp70","waveformEncrypted":"djEuYnR3aFJtQlk2TW5hTDFHdnhsMlh4VS9hT2lrSTZTdWdML1BMeTc0d2d2Y2dSYjM1Z0ErdTNQS1l6UW8ra2kraFNPMTg2VlE9","source":"WITHINGS","externalRecordingId":"round-trip-signal-1","recordedAt":"2026-06-15T14:22:00.000Z","samplingFrequency":300,"sampleCount":8,"durationSeconds":0.02666666666666667,"lead":"I","averageHeartRate":88,"rhythmClassification":"IRREGULAR","measurementId":"cmvv63cr30002i5ovjtizk643","createdAt":"2026-10-30T16:09:08.176Z","updatedAt":"2026-10-30T16:09:08.176Z"}],"symptomDefinitions":[{"id":"cmvv63csi0016i5ovvmag293a","labelEncrypted":"djEucUl6OFBiemEyODFRbFM0bndNRDg3c3ZoS0xmRkhmMWg3UTZma1lHNk1TST0=","icon":"Zap","sortOrder":0,"isActive":true,"createdAt":"2026-10-30T16:09:08.226Z","updatedAt":"2026-10-30T16:09:08.226Z","events":[{"id":"cmvv63csk0018i5ov281qfbid","noteEncrypted":null,"occurredAt":"2026-06-21T09:00:00.000Z","intensity":4,"episodeId":"cmvv63cse0012i5ov1p1u7q45","createdAt":"2026-10-30T16:09:08.228Z","updatedAt":"2026-10-30T16:09:08.228Z"},{"id":"cmvv63csl001ai5ovzt1uomqg","noteEncrypted":null,"occurredAt":"2026-07-10T18:30:00.000Z","intensity":2,"episodeId":null,"createdAt":"2026-10-30T16:09:08.229Z","updatedAt":"2026-10-30T16:09:08.229Z"}]},{"id":"cmvv63csj0017i5ovj6jweia0","labelEncrypted":"djEuN0JDdHdCMTIwaUFEWFFEaGE1K0tyRFp4YUQvZFlYenFtMTZ0NFFEZ0tDTThQMWtU","icon":"Brain","sortOrder":1,"isActive":false,"createdAt":"2026-10-30T16:09:08.227Z","updatedAt":"2026-10-30T16:09:08.227Z","events":[{"id":"cmvv63csl0019i5ovse86mpys","noteEncrypted":"djEuc2JndjZlMC9sUjlkbERrVmZ5R3BzQVkweWZFamdGSnR0aXdWRTVqU1dpV3FYenMvVWZtV3M4cWZySW94c3M4RkV3YXp5VE09","occurredAt":"2026-06-21T11:00:00.000Z","intensity":8,"episodeId":null,"createdAt":"2026-10-30T16:09:08.229Z","updatedAt":"2026-10-30T16:09:08.229Z"}]}],"lifeEvents":[{"id":"cmvv63csm001bi5ovq24th6vn","titleEncrypted":"djEucjczVkg0aW5SRG5yMWFBdFB1VlhWdzVwMG1NZ1k5WTBTVkVVL2RhUGwzV2U1RklZeW9DN3dDL3Q4TEd2Y1E9PQ==","noteEncrypted":"djEuR08xRmg3TFJldVJpWFMvY21hVkJxSDFaUEwxUUcrOVpaK0Q4L0FHR2xzN1J1dzJTY0RkZWZzNlNKZFVBUDZRZXhGSGJoNDB6NTR3Tk1BPT0=","category":"HOME","startDate":"2023-09-01","endDate":null,"precision":"MONTH","createdAt":"2026-10-30T16:09:08.230Z","updatedAt":"2026-10-30T16:09:08.230Z","deletedAt":null},{"id":"cmvv63csn001ci5ovjl3pzvzl","titleEncrypted":"djEuVEl6b0xqRVBNckRWZG9UaXRKNndmaHJLYXRDSDhoQmlPbjU2M2U5aHhEVkdYeUpxbWZpejExNVo=","noteEncrypted":null,"category":"WORK","startDate":"2026-03-02","endDate":"2026-05-29","precision":"DAY","createdAt":"2026-10-30T16:09:08.231Z","updatedAt":"2026-10-30T16:09:08.231Z","deletedAt":null}],"labResultDocumentLinks":[{"labResultId":"cmvv63csa000yi5ov0b3oitmb","documentId":"cmvv63ct3001qi5ovamnyn8l9","createdAt":"2026-10-30T16:09:08.270Z"}],"encounterMedicationLinks":[{"encounterId":"cmvv63ctk0026i5ov46w28i9w","medicationId":"cmvv63crh0005i5ovl2urjmny","createdAt":"2026-10-30T16:09:08.271Z"}],"encounterVaccinationLinks":[{"encounterId":"cmvv63ctk0026i5ov46w28i9w","vaccinationId":"cmvv63cto002bi5ov33yw9t32","createdAt":"2026-10-30T16:09:08.271Z"}],"medicationDocumentLinks":[{"medicationId":"cmvv63crh0005i5ovl2urjmny","documentId":"cmvv63ct3001qi5ovamnyn8l9","createdAt":"2026-10-30T16:09:08.272Z"}],"medicationConditionLinks":[{"medicationId":"cmvv63crh0005i5ovl2urjmny","episodeId":"cmvv63cse0012i5ov1p1u7q45","createdAt":"2026-10-30T16:09:08.273Z"}],"nutrientDays":[{"day":"2026-07-01","nutrient":"WATER","amount":2100,"unit":"ml","source":"MANUAL","externalSourceVersion":null,"createdAt":"2026-10-30T16:09:08.246Z","updatedAt":"2026-10-30T16:09:08.246Z"}]} \ No newline at end of file diff --git a/tests/fixtures/golden-backups/v1.46.5.manifest.json b/tests/fixtures/golden-backups/v1.46.5.manifest.json new file mode 100644 index 000000000..6975ca39d --- /dev/null +++ b/tests/fixtures/golden-backups/v1.46.5.manifest.json @@ -0,0 +1,72 @@ +{ + "release": "1.46.5", + "schemaVersion": "2", + "userId": "round-trip-owner", + "exportedAt": "2026-10-30T16:09:08.289Z", + "sha256": "4af553ea51571c60839e94f9fda740f90336de5ad5b2b3a25d9669a55ab88145", + "bytes": 41985, + "counts": { + "measurements": 5, + "medications": 1, + "intakeEvents": 1, + "medicationSideEffects": 1, + "medicationPauseEras": 2, + "medicationDoseChanges": 2, + "medicationInventoryItems": 2, + "medicationInventoryEvents": 2, + "reminderPhaseConfigs": 1, + "medicationEfficacyTargets": 1, + "medicationScheduleRevisions": 3, + "moodEntries": 1, + "customMoodTagCategories": 1, + "hiddenMoodTags": 1, + "cycles": 2, + "cycleDayLogs": 1, + "nutrientDays": 1, + "labResults": 1, + "biomarkers": 1, + "illnessEpisodes": 2, + "illnessDayLogs": 1, + "allergies": 1, + "familyHistory": 1, + "workouts": 1, + "documents": 1, + "healthProfile": 1, + "customMetrics": 1, + "healthProfileFactRevisions": 1, + "customMetricEntries": 2, + "correlationPatterns": 1, + "intradayProfiles": 1, + "healthScoreRecords": 1, + "onboardingRecords": 1, + "practitioners": 1, + "encounters": 1, + "encounterLinks": 3, + "customVaccines": 1, + "vaccinations": 1, + "vaccinationLinks": 1, + "measurementReminders": 1, + "measurementReminderEvents": 2, + "coachConversations": 2, + "coachMessages": 3, + "coachConversationDocuments": 1, + "coachFacts": 1, + "coachPlans": 1, + "coachReminders": 1, + "mentalHealthAssessments": 1, + "consentReceipts": 2, + "documentConditionLinks": 1, + "extractedFacts": 1, + "documentImportKeys": 1, + "personalRecords": 3, + "userAchievements": 2, + "environmentContexts": 2, + "environmentTravelLocations": 1, + "dawarichDays": 1, + "ecgRecordings": 1, + "symptomDefinitions": 2, + "symptomEvents": 3, + "lifeEvents": 2, + "relationLinks": 5 + } +} From 685cb2fa01ff4b816fb6a9640d426b946e3c4ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 30 Oct 2026 17:13:09 +0100 Subject: [PATCH 4/4] fix(ai): learn the max_tokens refusal from the field the request carried A call that ran beside the one that learned the lesson read today's selection instead and failed with the 400. The matcher now also wants the refusal to name max_tokens, so a refusal of another field that echoes the request does not switch the token field. Refs #1304 --- .../ai/__tests__/azure-token-budget.test.ts | 29 +++++++++++++++++++ src/lib/ai/openai-capabilities.ts | 9 ++++-- src/lib/ai/openai-client.ts | 27 ++++++++--------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/lib/ai/__tests__/azure-token-budget.test.ts b/src/lib/ai/__tests__/azure-token-budget.test.ts index 685513d85..8bca021ac 100644 --- a/src/lib/ai/__tests__/azure-token-budget.test.ts +++ b/src/lib/ai/__tests__/azure-token-budget.test.ts @@ -197,11 +197,40 @@ describe("an endpoint that refuses max_tokens", () => { expect(mock).toHaveBeenCalledTimes(1); }); + it("asks again on every call that sent max_tokens, also beside one that learned it", async () => { + const mock = vi + .fn() + .mockResolvedValueOnce(refused(400, AZURE_REFUSAL)) + .mockResolvedValueOnce(refused(400, AZURE_REFUSAL)) + .mockResolvedValue(ok()); + vi.stubGlobal("fetch", mock); + const client = gateway("prod-chat"); + const results = await Promise.allSettled([ + client.generateCompletion(turn()), + client.generateCompletion(turn()), + ]); + expect(results.map((r) => r.status)).toEqual(["fulfilled", "fulfilled"]); + expect(mock).toHaveBeenCalledTimes(4); + }); + it("reads only a 400 that names the field", () => { expect(isMaxTokensRejection(400, AZURE_REFUSAL)).toBe(true); expect(isMaxTokensRejection(422, AZURE_REFUSAL)).toBe(false); expect( isMaxTokensRejection(400, '{"error":"Unsupported parameter: top_k"}'), ).toBe(false); + // A refusal of another field that echoes the request. + expect( + isMaxTokensRejection( + 400, + '{"error":"Unsupported parameter: reasoning_effort is not supported","request":{"max_tokens":1000}}', + ), + ).toBe(false); + expect( + isMaxTokensRejection( + 400, + '{"error":"max_tokens is not supported with this model"}', + ), + ).toBe(true); }); }); diff --git a/src/lib/ai/openai-capabilities.ts b/src/lib/ai/openai-capabilities.ts index 6848722ee..3ff7014d8 100644 --- a/src/lib/ai/openai-capabilities.ts +++ b/src/lib/ai/openai-capabilities.ts @@ -99,14 +99,17 @@ export function resetMaxTokensRefusals(): void { /** * True when a 400 says the model takes `max_completion_tokens` and not - * `max_tokens`. A complaint about the value ("max_tokens is too large") is - * not a refusal of the field and does not match. + * `max_tokens`: it names the replacement, or it refuses `max_tokens` by + * name. A complaint about the value ("max_tokens is too large") or about + * another field that echoes the request is not a refusal of the field. */ export function isMaxTokensRejection(status: number, body: string): boolean { if (status !== 400 || !/\bmax_tokens\b/.test(body)) return false; return ( /max_completion_tokens/.test(body) || - /unsupported[\s_-]*parameter|not supported/i.test(body) + /unsupported[\s_-]*parameter\W{0,4}max_tokens\b|\bmax_tokens\W{0,4}(is\s+)?(not\s+supported|unsupported)/i.test( + body, + ) ); } diff --git a/src/lib/ai/openai-client.ts b/src/lib/ai/openai-client.ts index c4430706a..9bdb33623 100644 --- a/src/lib/ai/openai-client.ts +++ b/src/lib/ai/openai-client.ts @@ -206,11 +206,11 @@ export class OpenAIClient implements AIProvider { ? jsonModeDialectFor(this.config.baseUrl) : "response_format"; const sendJsonFormat = useJsonFormat && jsonDialect === "response_format"; - const capabilities = selectOpenAIChatCompletionsCapabilities( + const caps = selectOpenAIChatCompletionsCapabilities( this.config.baseUrl, this.config.model, ); - const reasoningWire = this.planReasoning(params, hasTools, capabilities); + const reasoningWire = this.planReasoning(params, hasTools, caps); // #1126 — the gateway entry's own setting, for a call without a level, // unless the wire carries one or the endpoint refused the field (#1279). const entryEffort = @@ -225,13 +225,13 @@ export class OpenAIClient implements AIProvider { reasoningWire.kind !== "none" ? reasoningWire.effort : entryEffort; const thinkingTokens = this.thinkingRoom(params, reasoningWire); const tokenBudget = (params.maxTokens ?? 1000) + thinkingTokens; - const capabilityParams = capabilities.supportsSamplingControls + const capabilityParams = caps.supportsSamplingControls ? { - [capabilities.tokenBudgetField]: tokenBudget, + [caps.tokenBudgetField]: tokenBudget, temperature: params.temperature ?? 0.3, ...(params.seed !== undefined ? { seed: params.seed } : {}), } - : { [capabilities.tokenBudgetField]: tokenBudget }; + : { [caps.tokenBudgetField]: tokenBudget }; // A gateway that needs no bearer (a LAN LiteLLM without a master key) // gets no `Authorization` header at all rather than an empty one — an @@ -299,7 +299,7 @@ export class OpenAIClient implements AIProvider { ); if (!res.ok) { - return this.failed(res, params, hops, { sent, sendJsonFormat }); + return this.failed(res, params, hops, { sent, sendJsonFormat, caps }); } // v1.33.1 (#470) — the standard `response_format` request succeeded: pin @@ -512,7 +512,11 @@ export class OpenAIClient implements AIProvider { res: Response, params: CompletionParams, hops: RequestHops, - sent: { sent: string | null; sendJsonFormat: boolean }, + sent: { + sent: string | null; + sendJsonFormat: boolean; + caps: OpenAIChatCompletionsCapabilities; + }, ): Promise { const bodyExcerpt = sanitiseBodyExcerpt(await res.text().catch(() => "")); // The entry's own setting (#1126) included: a refused field is learned @@ -535,13 +539,10 @@ export class OpenAIClient implements AIProvider { this.isGateway && sent.sendJsonFormat ? { endpoint: this.config.baseUrl, action: "ai.compat.jsonDialect" } : null, - // The request that failed carried `max_tokens`: nothing is learned - // between sending it and reading its refusal, so the selection holds. + // The field this request carried, not today's selection: a call + // running beside it may have learned the lesson meanwhile. budget: - selectOpenAIChatCompletionsCapabilities( - this.config.baseUrl, - this.config.model, - ).tokenBudgetField === "max_tokens" + sent.caps.tokenBudgetField === "max_tokens" ? { endpoint: this.config.baseUrl } : null, });