Merge pull request #1305 from MBombeck/release/v1.46.5

v1.46.5: Azure OpenAI reasoning deployments
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-10-30 17:44:07 +01:00
commit eb47f04a66
13 files changed
+460 -22

No files matched your search

+15
View File
@@ -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
+1 -1
View File
@@ -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.
+12
View File
@@ -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://<resource>.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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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/*`
@@ -0,0 +1,236 @@
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<typeof import("undici")>();
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<typeof vi.fn>, 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("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);
});
});
+3 -3
View File
@@ -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;
+2
View File
@@ -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);
+75 -2
View File
@@ -47,16 +47,89 @@ 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://<resource>.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<string>();
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`: 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\W{0,4}max_tokens\b|\bmax_tokens\W{0,4}(is\s+)?(not\s+supported|unsupported)/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;
}
+19 -8
View File
@@ -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
@@ -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`.
@@ -511,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<CompletionResult> {
const bodyExcerpt = sanitiseBodyExcerpt(await res.text().catch(() => ""));
// The entry's own setting (#1126) included: a refused field is learned
@@ -534,6 +539,12 @@ export class OpenAIClient implements AIProvider {
this.isGateway && sent.sendJsonFormat
? { endpoint: this.config.baseUrl, action: "ai.compat.jsonDialect" }
: null,
// The field this request carried, not today's selection: a call
// running beside it may have learned the lesson meanwhile.
budget:
sent.caps.tokenBudgetField === "max_tokens"
? { endpoint: this.config.baseUrl }
: null,
});
if (lesson && hops.canSend) return this.generateCompletion(params, hops);
const modelNotServed = await this.modelNoLongerServed(res.status);
+22 -6
View File
@@ -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(
+1
View File
@@ -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"Line truncated
+72
View File
@@ -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
}
}