mixdog 0.9.166 → 0.9.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/audit-usage-ledger.mjs +86 -0
- package/scripts/generate-voice-runtime-manifest.mjs +22 -17
- package/scripts/memory-chunk-quality.mjs +128 -0
- package/scripts/memory-layer2-check.mjs +133 -0
- package/scripts/model-catalog-audit.mjs +6 -0
- package/scripts/native-tool-download.mjs +14 -0
- package/scripts/prepare-native-assets.mjs +2 -7
- package/scripts/tool-failures.mjs +7 -2
- package/scripts/verify-release-assets.mjs +48 -90
- package/src/defaults/skills/browser-use/SKILL.md +33 -18
- package/src/defaults/skills/computer-use/SKILL.md +16 -8
- package/src/lib/keychain-cjs.cjs +0 -1
- package/src/lib/rules-builder.cjs +4 -1
- package/src/rules/agent/40-cycle1-agent.md +21 -13
- package/src/rules/lead/01-general.md +8 -3
- package/src/rules/shared/10-tool-workflow.md +20 -11
- package/src/rules/shared/30-exploration.md +11 -4
- package/src/rules/shared/40-editing.md +2 -3
- package/src/rules/shared/50-execution.md +6 -3
- package/src/rules/shared/60-verification.md +3 -2
- package/src/rules/shared/70-delivery.md +0 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
- package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
- package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
- package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
- package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
- package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
- package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
- package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/bridge-discovery.mjs +8 -0
- package/src/runtime/browser-bridge/action-schema.mjs +37 -43
- package/src/runtime/browser-bridge/client.mjs +111 -99
- package/src/runtime/browser-bridge/input-fields.mjs +187 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
- package/src/runtime/computer-bridge/action-schema.mjs +2 -46
- package/src/runtime/computer-bridge/client.mjs +69 -77
- package/src/runtime/computer-bridge/core-actions.mjs +7 -5
- package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
- package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
- package/src/runtime/github/commands.mjs +4 -1
- package/src/runtime/github/contract.mjs +1 -1
- package/src/runtime/github/tool.mjs +2 -2
- package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
- package/src/runtime/media/adapters/gemini-image.mjs +20 -11
- package/src/runtime/media/auth.mjs +12 -0
- package/src/runtime/media/catalog.mjs +30 -10
- package/src/runtime/media/jobs.mjs +4 -0
- package/src/runtime/media/lanes.mjs +40 -31
- package/src/runtime/memory/index.mjs +2 -9
- package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
- package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
- package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
- package/src/runtime/memory/lib/memory.mjs +2 -0
- package/src/runtime/memory/lib/query-handlers.mjs +98 -144
- package/src/runtime/memory/lib/query-ranking.mjs +23 -9
- package/src/runtime/memory/lib/recall-format.mjs +37 -27
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
- package/src/runtime/memory/lib/session-ingest.mjs +45 -84
- package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
- package/src/runtime/office/core/office-actions-batch.mjs +2 -8
- package/src/runtime/office/core/office-actions-open.mjs +124 -0
- package/src/runtime/office/core/office-actions-read.mjs +127 -0
- package/src/runtime/office/core/office-core.mjs +80 -1
- package/src/runtime/office/core/office-sessions.mjs +116 -138
- package/src/runtime/office/index.mjs +18 -271
- package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
- package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
- package/src/runtime/office/tool-defs.mjs +7 -6
- package/src/runtime/shared/agent-route-config.mjs +8 -8
- package/src/runtime/shared/child-spawn-gate.mjs +10 -13
- package/src/runtime/shared/config.mjs +2 -7
- package/src/runtime/shared/env.mjs +3 -3
- package/src/runtime/shared/err-text.mjs +1 -1
- package/src/runtime/shared/extension-scopes.mjs +3 -2
- package/src/runtime/shared/heap-cap.mjs +10 -6
- package/src/runtime/shared/llm/cost.mjs +35 -10
- package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
- package/src/runtime/shared/llm/usage-context.mjs +6 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
- package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
- package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
- package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
- package/src/runtime/shared/native-asset.mjs +3 -4
- package/src/runtime/shared/object.mjs +4 -0
- package/src/runtime/shared/pg-schema-db.mjs +21 -0
- package/src/runtime/shared/plugin-metadata.mjs +3 -1
- package/src/runtime/shared/resource-admission.mjs +1 -6
- package/src/runtime/shared/schedules-db.mjs +8 -24
- package/src/runtime/shared/schema-value-error.d.mts +2 -0
- package/src/runtime/shared/schema-value-error.mjs +68 -0
- package/src/runtime/shared/singleton-owner.mjs +11 -24
- package/src/runtime/shared/staged-update.mjs +1 -10
- package/src/runtime/shared/tool-card-model.mjs +8 -5
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +1 -5
- package/src/runtime/shared/user-data-guard.mjs +1 -4
- package/src/runtime/shared/webhooks-db.mjs +8 -24
- package/src/runtime/web-search/index.mjs +32 -8
- package/src/runtime/web-search/tool-defs.mjs +4 -4
- package/src/session-runtime/config-lifecycle.mjs +34 -0
- package/src/session-runtime/context-status.mjs +19 -0
- package/src/session-runtime/cwd-plugins.mjs +12 -29
- package/src/session-runtime/goal-tasks.mjs +2 -2
- package/src/session-runtime/goal-tool-defs.mjs +3 -2
- package/src/session-runtime/internal-tool-executor.mjs +7 -0
- package/src/session-runtime/lifecycle-api.mjs +5 -11
- package/src/session-runtime/notification-bus.mjs +41 -0
- package/src/session-runtime/prewarm.mjs +22 -0
- package/src/session-runtime/runtime-bootstrap.mjs +90 -0
- package/src/session-runtime/runtime-core.mjs +88 -202
- package/src/session-runtime/runtime-feature-gates.mjs +37 -0
- package/src/session-runtime/session-lifecycle.mjs +5 -5
- package/src/session-runtime/session-turn-api.mjs +1 -12
- package/src/session-runtime/setup-tool/executor.mjs +13 -5
- package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
- package/src/session-runtime/tool-defs.mjs +73 -4
- package/src/session-runtime/usage-stats-api.mjs +4 -1
- package/src/session-runtime/warmup-schedulers.mjs +8 -2
- package/src/session-runtime/workflow.mjs +14 -0
- package/src/standalone/channel-session-router.mjs +3 -1
- package/src/standalone/channel-worker.mjs +18 -9
- package/src/standalone/daemon-crash-capture.mjs +534 -0
- package/src/standalone/daemon-telemetry.mjs +175 -0
- package/src/standalone/daemon.mjs +31 -21
- package/src/standalone/session-client.mjs +31 -23
- package/src/standalone/usage-dashboard-model.mjs +2 -0
- package/src/standalone/usage-stats-hours.mjs +18 -11
- package/src/standalone/usage-stats-model.mjs +39 -28
- package/src/standalone/usage-stats-period.mjs +43 -45
- package/src/tui/app/use-transcript-window.mjs +1 -1
- package/src/tui/components/StatusLine.jsx +13 -42
- package/src/tui/dist/index.mjs +256 -239
- package/src/tui/session/session-api-ext.mjs +44 -41
- package/src/tui/session/session-api.mjs +4 -0
- package/src/tui/session/tool-card-results.mjs +25 -78
- package/src/tui/session/tool-result-status.mjs +94 -1
- package/src/tui/session/turn-aggregate-cards.mjs +17 -54
- package/src/ui/statusline-format.mjs +6 -26
- package/src/workflows/default/WORKFLOW.md +5 -0
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* Pricing is pulled from the LiteLLM catalog (already warmed by providers/
|
|
5
5
|
* agent bootstrap). All four token slots — input / output / cacheRead /
|
|
6
6
|
* cacheWrite — are multiplied by their matching $/M rate from the catalog
|
|
7
|
-
* and summed.
|
|
7
|
+
* and summed. A missing rate for a used token slot leaves the cost unknown.
|
|
8
8
|
*
|
|
9
9
|
* The catalog is looked up synchronously: if it has not been warmed yet
|
|
10
|
-
* (fresh process, first call), this returns
|
|
10
|
+
* (fresh process, first call), this returns an unknown price. The next
|
|
11
11
|
* call will pick up the cache.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { getModelMetadataSync } from '../../agent/orchestrator/providers/model-catalog.mjs';
|
|
14
|
+
import { getModelMetadataSync, resolveModelPricingIdentity } from '../../agent/orchestrator/providers/model-catalog.mjs';
|
|
15
|
+
import { PRICING_RATE_KEYS, ratesForPrompt } from '../../agent/orchestrator/providers/model-pricing-rates.mjs';
|
|
15
16
|
|
|
16
17
|
// OpenAI OAuth / OpenAI API / Gemini report `input_tokens` as the total prompt token
|
|
17
18
|
// count *including* the cached portion (inclusive). Anthropic reports the
|
|
@@ -47,10 +48,29 @@ export function priceUsage(args) {
|
|
|
47
48
|
const cached = n(args.cacheReadTokens);
|
|
48
49
|
const written = n(args.cacheWriteTokens);
|
|
49
50
|
const inclusive = args.inputTokensInclusive ?? isInclusiveProvider(args.provider);
|
|
50
|
-
const input = args.uncachedInputTokens != null ? n(args.uncachedInputTokens)
|
|
51
|
+
const input = args.inputTokensKnown === false ? 0 : args.uncachedInputTokens != null ? n(args.uncachedInputTokens)
|
|
51
52
|
: inclusive ? Math.max(0, n(args.inputTokens) - cached - written) : n(args.inputTokens);
|
|
52
|
-
const
|
|
53
|
-
|
|
53
|
+
const identity = resolveModelPricingIdentity(args.model, args.provider, args);
|
|
54
|
+
const meta = getModelMetadataSync(identity.pricingModel, args.provider);
|
|
55
|
+
const provenance = {
|
|
56
|
+
...identity,
|
|
57
|
+
pricingProvider: meta?.pricingProvider || identity.pricingProvider,
|
|
58
|
+
pricingSource: meta?.pricingSource || null,
|
|
59
|
+
...(args.inputTokensKnown === false ? { inputTokensKnown: false } : {}),
|
|
60
|
+
...(args.fast ? { fast: true } : {}),
|
|
61
|
+
...(args.serviceTier ? { serviceTier: args.serviceTier } : {}),
|
|
62
|
+
};
|
|
63
|
+
if (args.inputTokensKnown === false || !meta) return {
|
|
64
|
+
input, costUsd: null,
|
|
65
|
+
rates: { ...provenance, unpricedReason: args.inputTokensKnown === false ? 'unmeasured-input' : 'model-not-found' },
|
|
66
|
+
};
|
|
67
|
+
const promptTokens = input + cached + written;
|
|
68
|
+
if (args.historicalAggregate && (meta.pricingTiers?.some((tier) => promptTokens > tier.aboveInputTokens)
|
|
69
|
+
|| (meta.longContextThreshold && promptTokens >= meta.longContextThreshold))) {
|
|
70
|
+
// A daily sum cannot establish which individual requests crossed a
|
|
71
|
+
// context boundary. Do not price the whole day as one huge prompt.
|
|
72
|
+
return { input, costUsd: null, rates: { ...provenance, unpricedReason: 'request-boundaries-unavailable' } };
|
|
73
|
+
}
|
|
54
74
|
let multiplier = 1;
|
|
55
75
|
if (meta.longContextThreshold && input + cached + written >= meta.longContextThreshold) {
|
|
56
76
|
multiplier *= meta.longContextMultiplier || 1;
|
|
@@ -64,11 +84,16 @@ export function priceUsage(args) {
|
|
|
64
84
|
&& ((h >= 1 && h < 4) || (h >= 6 && h < 10));
|
|
65
85
|
if (!peak) multiplier *= meta.offPeakMultiplier;
|
|
66
86
|
}
|
|
67
|
-
if (
|
|
68
|
-
const keys =
|
|
87
|
+
if (identity.pricingModel === 'claude-opus-4-8' && (args.fast || args.serviceTier === 'fast')) multiplier *= 2;
|
|
88
|
+
const keys = PRICING_RATE_KEYS;
|
|
69
89
|
const tokens = [input, n(args.outputTokens), cached, written];
|
|
70
|
-
const
|
|
71
|
-
|
|
90
|
+
const tierRates = ratesForPrompt(meta, promptTokens);
|
|
91
|
+
const rates = { ...provenance, ...Object.fromEntries(keys.map((key) =>
|
|
92
|
+
[key, tierRates[key] == null ? null : tierRates[key] * multiplier])) };
|
|
93
|
+
const missingRates = keys.filter((key, i) => tokens[i] > 0 && rates[key] === null);
|
|
94
|
+
if (missingRates.length) {
|
|
95
|
+
rates.unpricedReason = 'missing-rate';
|
|
96
|
+
rates.missingRates = missingRates;
|
|
72
97
|
return { input, costUsd: null, rates };
|
|
73
98
|
}
|
|
74
99
|
const costUsd = tokens.reduce((sum, amount, i) => sum + amount * (rates[keys[i]] ?? 0), 0) / 1_000_000;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { getUsageLedger, makeUsageRecord } from './usage-ledger.mjs';
|
|
3
|
+
import { withUsageContext } from './usage-context.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Runs at the common provider boundary, not inside optional diagnostic IO.
|
|
@@ -23,6 +24,8 @@ export async function accountProviderSend(provider, instance, send, model, opts
|
|
|
23
24
|
const row = makeUsageRecord({
|
|
24
25
|
id: result.responseId ? undefined : requestId,
|
|
25
26
|
ts: Date.now(), provider, model: result.model || model,
|
|
27
|
+
requestedModel: model,
|
|
28
|
+
pricingModel: result.pricingModel,
|
|
26
29
|
sessionId: opts.sessionId || opts.session?.id,
|
|
27
30
|
sourceType: opts.session?.sourceType || opts.sourceType || opts.requestKind || '',
|
|
28
31
|
inputTokens: usage.inputTokens,
|
|
@@ -47,7 +50,14 @@ export async function accountProviderSend(provider, instance, send, model, opts
|
|
|
47
50
|
}
|
|
48
51
|
};
|
|
49
52
|
let result;
|
|
50
|
-
try {
|
|
53
|
+
try {
|
|
54
|
+
result = await withUsageContext({
|
|
55
|
+
provider, requestedModel: model,
|
|
56
|
+
sessionId: opts.sessionId || opts.session?.id,
|
|
57
|
+
sourceType: opts.session?.sourceType || opts.sourceType || opts.requestKind || '',
|
|
58
|
+
inputTokensInclusive: instance.constructor?.inputExcludesCache !== true,
|
|
59
|
+
}, send);
|
|
60
|
+
}
|
|
51
61
|
catch (error) {
|
|
52
62
|
// Only provider-reported partial usage is recordable; never invent
|
|
53
63
|
// tokens for a failed request or reinterpret an error as a success.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
// The selected route owns accounting identity, not an inner transport adapter.
|
|
4
|
+
const context = new AsyncLocalStorage();
|
|
5
|
+
export const withUsageContext = (identity, send) => context.run(identity, send);
|
|
6
|
+
export const currentUsageContext = () => context.getStore();
|
|
@@ -29,6 +29,8 @@ export function importTraceRow(row) {
|
|
|
29
29
|
: /^\d+$/.test(String(row.ts)) ? Number(row.ts) : Date.parse(row.ts);
|
|
30
30
|
return makeUsageRecord({
|
|
31
31
|
ts, provider, model: row.model,
|
|
32
|
+
requestedModel: row.requested_model || payload.requested_model,
|
|
33
|
+
pricingModel: row.pricing_model || payload.pricing_model,
|
|
32
34
|
inputTokens: input, outputTokens: output,
|
|
33
35
|
uncachedInputTokens: raw ? row.uncached_input_tokens ?? payload.uncached_input_tokens : undefined,
|
|
34
36
|
cacheReadTokens: raw ? row.cached_tokens : row.cacheReadTokens,
|
|
@@ -61,6 +63,7 @@ export function repriceRestoredDays(original) {
|
|
|
61
63
|
uncachedInputTokens: bucket.input, outputTokens: bucket.output,
|
|
62
64
|
cacheReadTokens: bucket.cacheRead, cacheWriteTokens: bucket.cacheWrite,
|
|
63
65
|
historical: true,
|
|
66
|
+
historicalAggregate: true,
|
|
64
67
|
});
|
|
65
68
|
bucket.costUsd = price.costUsd ?? 0;
|
|
66
69
|
bucket.costBilled = 0;
|
|
@@ -145,6 +148,8 @@ export async function readOriginalTrace(dataDir, until) {
|
|
|
145
148
|
SELECT ts,kind,model,session_id,input_tokens,output_tokens,cached_tokens,cache_write_tokens,
|
|
146
149
|
duration_ms,jsonb_build_object(
|
|
147
150
|
'provider',payload->'provider',
|
|
151
|
+
'requested_model',payload->'requested_model',
|
|
152
|
+
'pricing_model',payload->'pricing_model',
|
|
148
153
|
'uncached_input_tokens',payload->'uncached_input_tokens',
|
|
149
154
|
'response_id',payload->'response_id',
|
|
150
155
|
'service_tier',payload->'service_tier',
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { priceUsage } from './cost.mjs';
|
|
6
|
+
import { usageRouteKind } from './usage-ledger.mjs';
|
|
7
|
+
import { normalizeUsageMeasurement } from './usage-measurement.mjs';
|
|
8
|
+
|
|
9
|
+
// Excludes only the two fields a repair may change: route attribution and price.
|
|
10
|
+
export function usageLedgerIntegrity(db) {
|
|
11
|
+
const hash = createHash('sha256');
|
|
12
|
+
let records = 0;
|
|
13
|
+
for (const row of db.prepare(`SELECT id,ts,day,session,input,output,cache_read,cache_write,duration_ms
|
|
14
|
+
FROM usage_events ORDER BY id`).iterate()) {
|
|
15
|
+
hash.update(JSON.stringify({ ...row, id: Buffer.from(row.id).toString('hex') }));
|
|
16
|
+
records++;
|
|
17
|
+
}
|
|
18
|
+
return { records, sha256: hash.digest('hex') };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bounded correction. Automatic use can only revisit unpriced records and
|
|
23
|
+
* takes a coherent backup under the writer reservation before its first write.
|
|
24
|
+
* Provider overrides require operator evidence; unknown model aliases are NOT
|
|
25
|
+
* inferred. Existing known prices remain fixed unless the billing route changes.
|
|
26
|
+
*/
|
|
27
|
+
export function repairUsageLedger(ledger, {
|
|
28
|
+
throughTs, providerOverrides = {}, price = priceUsage, onlyUnpriced = false, backup = false,
|
|
29
|
+
} = {}) {
|
|
30
|
+
if (!Number.isSafeInteger(throughTs) || throughTs <= 0) throw new Error('Repair requires an explicit timestamp bound');
|
|
31
|
+
for (const [from, to] of Object.entries(providerOverrides)) {
|
|
32
|
+
if (!from || typeof to !== 'string' || !to) throw new Error('Invalid provider correction');
|
|
33
|
+
}
|
|
34
|
+
if (onlyUnpriced && Object.keys(providerOverrides).length) throw new Error('Automatic repricing cannot change providers');
|
|
35
|
+
const db = ledger.db;
|
|
36
|
+
db.exec('BEGIN IMMEDIATE');
|
|
37
|
+
try {
|
|
38
|
+
const before = usageLedgerIntegrity(db);
|
|
39
|
+
const insertRoute = db.prepare('INSERT OR IGNORE INTO usage_routes(signature) VALUES (?)');
|
|
40
|
+
const findRoute = db.prepare('SELECT id FROM usage_routes WHERE signature=?');
|
|
41
|
+
const update = db.prepare('UPDATE usage_events SET route=?,cost_usd=? WHERE id=?');
|
|
42
|
+
const report = { reattributed: 0, repriced: 0, stillUnpriced: 0, changes: {} };
|
|
43
|
+
const changes = [];
|
|
44
|
+
const rows = db.prepare(`SELECT e.*,r.signature FROM usage_events e
|
|
45
|
+
JOIN usage_routes r ON r.id=e.route WHERE e.ts<=?
|
|
46
|
+
${onlyUnpriced ? "AND e.cost_usd IS NULL AND json_extract(r.signature,'$[4]')='unpriced'" : ''}`).all(throughTs);
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const signature = JSON.parse(row.signature);
|
|
49
|
+
const originalProvider = signature[0];
|
|
50
|
+
const provider = Object.hasOwn(providerOverrides, originalProvider)
|
|
51
|
+
? providerOverrides[originalProvider] : originalProvider;
|
|
52
|
+
const moved = provider !== originalProvider;
|
|
53
|
+
const kind = moved ? usageRouteKind(provider) : signature[2];
|
|
54
|
+
let cost = row.cost_usd;
|
|
55
|
+
const oldRates = signature[5] ? JSON.parse(signature[5]) : null;
|
|
56
|
+
if (moved || signature[4] === 'unpriced') {
|
|
57
|
+
const priced = kind === 'local' ? { costUsd: 0, rates: null } : price({
|
|
58
|
+
provider, model: signature[1], pricingModel: oldRates?.pricingModel,
|
|
59
|
+
requestedModel: oldRates?.requestedModel,
|
|
60
|
+
inputTokensKnown: oldRates?.inputTokensKnown === false
|
|
61
|
+
|| normalizeUsageMeasurement(provider, { turns: 1 }).unmeasuredTurns ? false : undefined,
|
|
62
|
+
uncachedInputTokens: row.input, outputTokens: row.output,
|
|
63
|
+
cacheReadTokens: row.cache_read, cacheWriteTokens: row.cache_write,
|
|
64
|
+
fast: oldRates?.fast, serviceTier: oldRates?.serviceTier,
|
|
65
|
+
ts: row.ts, historical: true,
|
|
66
|
+
});
|
|
67
|
+
cost = priced.costUsd;
|
|
68
|
+
if (cost !== null && (!Number.isFinite(cost) || cost < 0)) throw new Error('Invalid repair price');
|
|
69
|
+
signature[4] = kind === 'local' ? 'local' : cost === null ? 'unpriced'
|
|
70
|
+
: kind === 'oauth' || kind === 'quota-api' ? 'subscription' : 'catalog';
|
|
71
|
+
signature[5] = priced.rates ? JSON.stringify(priced.rates) : null;
|
|
72
|
+
if (row.cost_usd === null && cost !== null) report.repriced++;
|
|
73
|
+
}
|
|
74
|
+
signature[0] = provider;
|
|
75
|
+
signature[2] = kind;
|
|
76
|
+
if (moved) report.reattributed++;
|
|
77
|
+
if (cost === null) report.stillUnpriced++;
|
|
78
|
+
const encoded = JSON.stringify(signature);
|
|
79
|
+
if (encoded === row.signature && cost === row.cost_usd) continue;
|
|
80
|
+
changes.push({ row, encoded, cost });
|
|
81
|
+
const key = `${originalProvider} → ${provider} / ${signature[1]}`;
|
|
82
|
+
report.changes[key] = (report.changes[key] || 0) + 1;
|
|
83
|
+
}
|
|
84
|
+
if (changes.length && backup && ledger.path !== ':memory:') {
|
|
85
|
+
const directory = mkdtempSync(join(dirname(ledger.path), 'pricing-backup-'));
|
|
86
|
+
report.backupPath = join(directory, 'ledger.sqlite');
|
|
87
|
+
const reader = new DatabaseSync(ledger.path, { readOnly: true });
|
|
88
|
+
try { reader.prepare('VACUUM INTO ?').run(report.backupPath); }
|
|
89
|
+
finally { reader.close(); }
|
|
90
|
+
}
|
|
91
|
+
for (const { row, encoded, cost } of changes) {
|
|
92
|
+
insertRoute.run(encoded);
|
|
93
|
+
update.run(findRoute.get(encoded).id, cost, row.id);
|
|
94
|
+
}
|
|
95
|
+
if (changes.length) ledger.rebuildIndexes();
|
|
96
|
+
const after = usageLedgerIntegrity(db);
|
|
97
|
+
if (before.records !== after.records || before.sha256 !== after.sha256) {
|
|
98
|
+
throw new Error('Repair changed immutable request records');
|
|
99
|
+
}
|
|
100
|
+
if (changes.length) ledger.set('lastUsageRepair', JSON.stringify({
|
|
101
|
+
at: Date.now(), throughTs, providerOverrides, ...report, integrity: after,
|
|
102
|
+
}));
|
|
103
|
+
db.exec('COMMIT');
|
|
104
|
+
return { ...report, integrity: after };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
db.exec('ROLLBACK');
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -162,9 +162,7 @@ export function makeUsageRecord(args) {
|
|
|
162
162
|
const ts = Number(args.ts ?? Date.now());
|
|
163
163
|
if (!Number.isSafeInteger(ts) || ts <= 0) throw new Error('Usage requires a valid timestamp');
|
|
164
164
|
const kind = args.kind || usageRouteKind(provider);
|
|
165
|
-
const priced = args
|
|
166
|
-
? { input: 0, costUsd: null, rates: null }
|
|
167
|
-
: priceUsage({ ...args, provider, model, ts });
|
|
165
|
+
const priced = priceUsage({ ...args, provider, model, ts });
|
|
168
166
|
const supplied = args.costUsd !== null && args.costUsd !== undefined && args.costUsd !== ''
|
|
169
167
|
&& Number.isFinite(Number(args.costUsd)) && Number(args.costUsd) >= 0;
|
|
170
168
|
// OAuth/quota plans have value, not per-request invoices. A backend's
|
|
@@ -181,7 +179,10 @@ export function makeUsageRecord(args) {
|
|
|
181
179
|
costUsd,
|
|
182
180
|
costSource: kind === 'local' ? 'local' : costUsd === null ? 'unpriced'
|
|
183
181
|
: subscription ? 'subscription' : reported ? 'provider' : 'catalog',
|
|
184
|
-
rates: reported
|
|
182
|
+
rates: reported || kind === 'local' ? {
|
|
183
|
+
requestedModel: priced.rates.requestedModel, pricingModel: priced.rates.pricingModel,
|
|
184
|
+
pricingProvider: provider, pricingSource: kind === 'local' ? 'local' : 'provider',
|
|
185
|
+
} : priced.rates,
|
|
185
186
|
responseId: text(args.responseId),
|
|
186
187
|
origin: args.origin || 'live',
|
|
187
188
|
durationMs: number(args.durationMs),
|
|
@@ -192,6 +193,7 @@ export function makeUsageRecord(args) {
|
|
|
192
193
|
|
|
193
194
|
export class UsageLedger {
|
|
194
195
|
constructor(path) {
|
|
196
|
+
this.path = path;
|
|
195
197
|
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
196
198
|
this.db = new DatabaseSync(path);
|
|
197
199
|
this.db.exec('PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON;');
|
|
@@ -253,6 +255,27 @@ export class UsageLedger {
|
|
|
253
255
|
this.db.prepare('INSERT OR IGNORE INTO metadata VALUES (?,?)').run('liveSince', String(ts));
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
indexRecord(row, rank) {
|
|
259
|
+
const conversation = isConversationUsageSource(row.sourceType);
|
|
260
|
+
this.daily.run(row.day, rank, row.provider, row.model, row.kind, row.costSource,
|
|
261
|
+
Number(conversation), 1, row.input, row.output, row.cacheRead, row.cacheWrite,
|
|
262
|
+
row.costUsd ?? 0, row.durationMs, Number(row.origin !== 'live'));
|
|
263
|
+
if (conversation && row.sessionId) this.session.run(row.day, rank, row.sessionId,
|
|
264
|
+
row.input + row.output + row.cacheRead + row.cacheWrite);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Caller owns the transaction; only derived indexes are rebuilt.
|
|
268
|
+
rebuildIndexes() {
|
|
269
|
+
this.db.exec('DELETE FROM daily; DELETE FROM day_sessions;');
|
|
270
|
+
for (const row of this.db.prepare('SELECT * FROM events').iterate()) {
|
|
271
|
+
this.indexRecord({
|
|
272
|
+
...row, sourceType: row.source_type, sessionId: row.session_id,
|
|
273
|
+
costSource: row.cost_source, costUsd: row.cost_usd,
|
|
274
|
+
cacheRead: row.cache_read, cacheWrite: row.cache_write, durationMs: row.duration_ms,
|
|
275
|
+
}, row.rank);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
256
279
|
record(rows) {
|
|
257
280
|
let inserted = 0;
|
|
258
281
|
this.db.exec('BEGIN IMMEDIATE');
|
|
@@ -266,12 +289,7 @@ export class UsageLedger {
|
|
|
266
289
|
rates: row.rates ? JSON.stringify(row.rates) : null,
|
|
267
290
|
duration_ms: row.durationMs, rank,
|
|
268
291
|
})) continue;
|
|
269
|
-
|
|
270
|
-
this.daily.run(row.day, rank, row.provider, row.model, row.kind, row.costSource,
|
|
271
|
-
Number(conversation), 1, row.input, row.output, row.cacheRead, row.cacheWrite,
|
|
272
|
-
row.costUsd ?? 0, row.durationMs, Number(row.origin !== 'live'));
|
|
273
|
-
if (conversation && row.sessionId) this.session.run(row.day, rank, row.sessionId,
|
|
274
|
-
row.input + row.output + row.cacheRead + row.cacheWrite);
|
|
292
|
+
this.indexRecord(row, rank);
|
|
275
293
|
if (row.origin === 'live' && !this.get('liveSince')) this.set('liveSince', row.ts);
|
|
276
294
|
inserted += 1;
|
|
277
295
|
}
|
|
@@ -298,13 +316,14 @@ export class UsageLedger {
|
|
|
298
316
|
}
|
|
299
317
|
|
|
300
318
|
/** Read cached amounts; group retained attribution separately for distinct sessions. */
|
|
301
|
-
rollup({ hourlyDay = null, fromDay = '0000-01-01', toDay = '9999-12-31'
|
|
319
|
+
rollup({ hourlyDay = null, fromDay = '0000-01-01', toDay = '9999-12-31',
|
|
320
|
+
fromMs = null, toMs = null } = {}) {
|
|
302
321
|
const days = {};
|
|
303
322
|
const hourly = hourlyDay ? { rows: [], unallocated: [] } : null;
|
|
304
|
-
const fromTs = fromDay === '0000-01-01' ? 0 : new Date(`${fromDay}T00:00:00`).getTime();
|
|
323
|
+
const fromTs = fromMs ?? (fromDay === '0000-01-01' ? 0 : new Date(`${fromDay}T00:00:00`).getTime());
|
|
305
324
|
const end = toDay === '9999-12-31' ? null : new Date(`${toDay}T00:00:00`);
|
|
306
325
|
if (end) end.setDate(end.getDate() + 1);
|
|
307
|
-
const toTs = end ? end.getTime() : Number.MAX_SAFE_INTEGER;
|
|
326
|
+
const toTs = toMs == null ? (end ? end.getTime() : Number.MAX_SAFE_INTEGER) : toMs + 1;
|
|
308
327
|
const empty = () => ({
|
|
309
328
|
turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0,
|
|
310
329
|
costBilled: 0, costEstimated: 0, costKnownTurns: 0, durationMs: 0, durationTurns: 0,
|
|
@@ -335,17 +354,34 @@ export class UsageLedger {
|
|
|
335
354
|
const end = new Date(start);
|
|
336
355
|
end.setDate(end.getDate() + 1);
|
|
337
356
|
hourly.rows = this.db.prepare(`
|
|
338
|
-
SELECT e.ts,e.provider,e.source_type,e.input,e.output,e.cost_usd
|
|
357
|
+
SELECT e.ts,e.provider,e.source_type,e.input,e.output,e.cache_read,e.cache_write,e.cost_usd
|
|
339
358
|
FROM events e JOIN (${best}) b USING(day,provider,model,rank)
|
|
340
359
|
WHERE e.ts>=? AND e.ts<? ORDER BY e.ts
|
|
341
|
-
`).all(fromDay, toDay, start.getTime()
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
360
|
+
`).all(fromDay, toDay, fromMs == null ? start.getTime() : fromTs,
|
|
361
|
+
toMs == null ? end.getTime() : toTs).map((row) => {
|
|
362
|
+
const usage = normalizeUsageMeasurement(row.provider, { turns: 1, input: row.input,
|
|
363
|
+
cacheRead: row.cache_read, cacheWrite: row.cache_write, costUsd: row.cost_usd });
|
|
364
|
+
return { ...row, input: usage.input, cache_read: usage.cacheRead, cache_write: usage.cacheWrite,
|
|
365
|
+
cost_usd: usage.costUsd, unmeasuredTurns: usage.unmeasuredTurns || 0 };
|
|
346
366
|
});
|
|
347
367
|
}
|
|
348
|
-
|
|
368
|
+
// Partial calendar days must be rebuilt from retained timestamps;
|
|
369
|
+
// cached whole-day totals would include usage outside a rolling window.
|
|
370
|
+
const amounts = fromMs == null
|
|
371
|
+
? this.db.prepare(`SELECT d.* FROM daily d JOIN (${best}) b USING(day,provider,model,rank)`).all(fromDay, toDay)
|
|
372
|
+
: this.db.prepare(`
|
|
373
|
+
SELECT e.day,e.rank,e.provider,e.model,e.kind,e.cost_source,e.source_type,
|
|
374
|
+
COUNT(*) AS turns,SUM(e.input) AS input,SUM(e.output) AS output,
|
|
375
|
+
SUM(e.cache_read) AS cache_read,SUM(e.cache_write) AS cache_write,
|
|
376
|
+
SUM(e.cost_usd) AS cost_usd,SUM(e.duration_ms) AS duration_ms,
|
|
377
|
+
MAX(e.origin!='live') AS imported
|
|
378
|
+
FROM events e JOIN (${best}) b USING(day,provider,model,rank)
|
|
379
|
+
WHERE e.ts>=? AND e.ts<?
|
|
380
|
+
GROUP BY e.day,e.rank,e.provider,e.model,e.kind,e.cost_source,e.source_type
|
|
381
|
+
`).all(fromDay, toDay, fromTs, toTs).map((row) => ({
|
|
382
|
+
...row, conversation: isConversationUsageSource(row.source_type),
|
|
383
|
+
}));
|
|
384
|
+
for (const row of amounts) {
|
|
349
385
|
const day = days[row.day] ||= { ...empty(), models: {}, sessions: {}, conversation: empty() };
|
|
350
386
|
const key = `${row.provider}/${row.model}`;
|
|
351
387
|
const route = day.models[key] ||= {
|
|
@@ -395,6 +431,9 @@ export class UsageLedger {
|
|
|
395
431
|
}
|
|
396
432
|
}
|
|
397
433
|
}
|
|
434
|
+
// Timeless legacy totals cannot establish membership in a 24-hour window.
|
|
435
|
+
// They remain available in the calendar/history views.
|
|
436
|
+
if (fromMs != null) return hourly ? { days, hourly } : { days };
|
|
398
437
|
for (const row of this.db.prepare('SELECT day,document FROM legacy_days WHERE day BETWEEN ? AND ?').all(fromDay, toDay)) {
|
|
399
438
|
const legacy = normalizeLegacyUsageDay(JSON.parse(row.document));
|
|
400
439
|
const day = days[row.day];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { pricingCatalogRevisionSync } from '../../agent/orchestrator/providers/model-catalog.mjs';
|
|
2
|
+
import { repairUsageLedger } from './usage-ledger-repair.mjs';
|
|
3
|
+
|
|
4
|
+
const checked = new WeakMap();
|
|
5
|
+
|
|
6
|
+
function pendingFingerprint(ledger, revision) {
|
|
7
|
+
const pending = ledger.db.prepare(`SELECT COUNT(*) AS count,MAX(e.ts) AS latest
|
|
8
|
+
FROM usage_events e JOIN usage_routes r ON r.id=e.route
|
|
9
|
+
WHERE e.cost_usd IS NULL AND json_extract(r.signature,'$[4]')='unpriced'
|
|
10
|
+
AND e.ts<=?`).get(Date.now());
|
|
11
|
+
return { count: pending.count, key: `${revision}:${pending.count}:${pending.latest}` };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Price-table changes revisit unknown costs, never overwrite a known bill or
|
|
15
|
+
* estimate. New unknown records also get checked without polling old rows on
|
|
16
|
+
* every dashboard render. Failed repairs are not marked successful. */
|
|
17
|
+
export function refreshUnpricedUsage(ledger) {
|
|
18
|
+
const revision = pricingCatalogRevisionSync();
|
|
19
|
+
const pending = pendingFingerprint(ledger, revision);
|
|
20
|
+
if (!pending.count || checked.get(ledger) === pending.key) return { skipped: true };
|
|
21
|
+
const result = repairUsageLedger(ledger, {
|
|
22
|
+
throughTs: Date.now(), onlyUnpriced: true, backup: true,
|
|
23
|
+
});
|
|
24
|
+
checked.set(ledger, pendingFingerprint(ledger, revision).key);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
@@ -104,39 +104,35 @@ export function isConversationUsageSource(sourceType) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function emptyRouteTotals() {
|
|
107
|
-
return { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0
|
|
107
|
+
return { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0,
|
|
108
|
+
costBilled: 0, costEstimated: 0, costKnownTurns: 0 };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
function emptyTurnTotals() {
|
|
111
112
|
return {
|
|
112
113
|
...emptyRouteTotals(),
|
|
113
|
-
// Priced by the provider itself vs derived from the model catalog. A
|
|
114
|
-
// subscription turn is never invoiced per token, so merging the two would
|
|
115
|
-
// present a guess as a bill.
|
|
116
|
-
costBilled: 0,
|
|
117
|
-
costEstimated: 0,
|
|
118
|
-
costKnownTurns: 0,
|
|
119
114
|
durationMs: 0,
|
|
120
115
|
durationTurns: 0,
|
|
121
116
|
};
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
function addRouteTotals(target, turn) {
|
|
119
|
+
function addRouteTotals(target, turn, { priced, billed } = {}) {
|
|
125
120
|
target.turns += turn.turns;
|
|
126
121
|
target.input += turn.input;
|
|
127
122
|
target.output += turn.output;
|
|
128
123
|
target.cacheRead += turn.cacheRead;
|
|
129
124
|
target.cacheWrite += turn.cacheWrite;
|
|
130
125
|
target.costUsd = round6(target.costUsd + turn.costUsd);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function addTurnTotals(target, turn, { priced, billed, durationMs }) {
|
|
134
|
-
addRouteTotals(target, turn);
|
|
135
126
|
if (priced) {
|
|
136
127
|
target.costKnownTurns += 1;
|
|
137
128
|
if (billed) target.costBilled = round6(target.costBilled + turn.costUsd);
|
|
138
129
|
else target.costEstimated = round6(target.costEstimated + turn.costUsd);
|
|
139
130
|
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addTurnTotals(target, turn, pricing) {
|
|
134
|
+
addRouteTotals(target, turn, pricing);
|
|
135
|
+
const { durationMs } = pricing;
|
|
140
136
|
if (durationMs > 0) {
|
|
141
137
|
target.durationMs += durationMs;
|
|
142
138
|
target.durationTurns += 1;
|
|
@@ -181,6 +177,11 @@ function readRouteTotals(raw, target) {
|
|
|
181
177
|
target.cacheRead = num(raw?.cacheRead);
|
|
182
178
|
target.cacheWrite = num(raw?.cacheWrite);
|
|
183
179
|
target.costUsd = round6(num(raw?.costUsd));
|
|
180
|
+
target.costBilled = round6(num(raw?.costBilled));
|
|
181
|
+
target.costEstimated = round6(raw?.costEstimated == null
|
|
182
|
+
? Math.max(0, target.costUsd - target.costBilled) : num(raw.costEstimated));
|
|
183
|
+
target.costKnownTurns = raw?.costKnownTurns == null
|
|
184
|
+
? (target.costUsd > 0 ? target.turns : 0) : num(raw.costKnownTurns);
|
|
184
185
|
return target;
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -311,7 +312,8 @@ export function foldUsageRollup(current, summary, now = Date.now()) {
|
|
|
311
312
|
// costSource 'none' means neither the provider nor the catalog priced this
|
|
312
313
|
// turn; counting it keeps the surface honest about partial cost coverage.
|
|
313
314
|
const pricing = {
|
|
314
|
-
priced:
|
|
315
|
+
priced: summary?.costUsd != null && Number.isFinite(Number(summary.costUsd))
|
|
316
|
+
&& !['', 'none', 'unpriced'].includes(costSource),
|
|
315
317
|
billed: costSource === 'provider',
|
|
316
318
|
durationMs: num(summary?.durationMs),
|
|
317
319
|
};
|
|
@@ -340,10 +342,10 @@ export function foldUsageRollup(current, summary, now = Date.now()) {
|
|
|
340
342
|
if (known || Object.keys(day.models).length < MAX_MODELS_PER_DAY) {
|
|
341
343
|
const bucket = known ? day.models[modelKey] : emptyModel(provider, model);
|
|
342
344
|
bucket.kind = cleanId(summary?.providerKind) || bucket.kind;
|
|
343
|
-
addRouteTotals(bucket, turn);
|
|
345
|
+
addRouteTotals(bucket, turn, pricing);
|
|
344
346
|
if (conversation) {
|
|
345
347
|
if (!bucket.conversation) bucket.conversation = emptyRouteTotals();
|
|
346
|
-
addRouteTotals(bucket.conversation, turn);
|
|
348
|
+
addRouteTotals(bucket.conversation, turn, pricing);
|
|
347
349
|
}
|
|
348
350
|
day.models[modelKey] = bucket;
|
|
349
351
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readFileSync, readdirSync, renameSync, rmSync,
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { readJsonSafe } from './json-file.mjs';
|
|
13
14
|
import {
|
|
14
15
|
downloadToFileWithRetry,
|
|
15
16
|
MAX_NATIVE_BINARY_DOWNLOAD_BYTES,
|
|
@@ -26,9 +27,7 @@ export function binSuffix() {
|
|
|
26
27
|
return process.platform === 'win32' ? '.exe' : '';
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export
|
|
30
|
-
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
31
|
-
}
|
|
30
|
+
export { readJsonSafe as readJsonOrNull };
|
|
32
31
|
|
|
33
32
|
export function validSha256(value) {
|
|
34
33
|
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
|
|
@@ -73,7 +72,7 @@ export function validReleaseAsset(manifest, pkey, { name, tagPrefix }) {
|
|
|
73
72
|
|
|
74
73
|
export function readBundledManifest(bundledPath, options = {}) {
|
|
75
74
|
if (options.bundledManifest) return options.bundledManifest;
|
|
76
|
-
return existsSync(bundledPath) ?
|
|
75
|
+
return existsSync(bundledPath) ? readJsonSafe(bundledPath) : null;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
export async function fetchRemoteManifest(url, { fetch: fetchFn = fetch, label = '[native-asset]' } = {}) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Lazy per-dataDir PG handle: one in-flight init, bootstrap lock, retry after failure. */
|
|
2
|
+
export function createPgSchemaDb({ schema, ddl, defaultDataDir, ensurePg, withLock }) {
|
|
3
|
+
const ready = new Map();
|
|
4
|
+
return async function getDb(dataDir = defaultDataDir()) {
|
|
5
|
+
if (ready.has(dataDir)) return ready.get(dataDir);
|
|
6
|
+
const p = (async () => {
|
|
7
|
+
const { db, pool } = await ensurePg(dataDir, { schema });
|
|
8
|
+
// Serialize CREATE TABLE across concurrent first-boot processes on the
|
|
9
|
+
// same cluster-global advisory lock the adapter uses for schema bootstrap.
|
|
10
|
+
await withLock(pool, () => db.exec(ddl));
|
|
11
|
+
return db;
|
|
12
|
+
})();
|
|
13
|
+
ready.set(dataDir, p);
|
|
14
|
+
try {
|
|
15
|
+
return await p;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
ready.delete(dataDir); // let the next call retry after a transient failure
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPlainObject } from './object.mjs';
|
|
2
|
+
|
|
1
3
|
// Display-only manifest metadata, shared by registry and session status.
|
|
2
4
|
// Keep malformed values empty instead of leaking "[object Object]" into UI.
|
|
3
5
|
function text(value) {
|
|
@@ -5,7 +7,7 @@ function text(value) {
|
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
function object(value) {
|
|
8
|
-
return
|
|
10
|
+
return isPlainObject(value) ? value : {};
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function authorText(value) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { freemem, totalmem } from 'node:os';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
|
|
4
|
-
import { positiveInt } from './numbers.mjs';
|
|
4
|
+
import { nonNegativeInt, positiveInt } from './numbers.mjs';
|
|
5
5
|
|
|
6
6
|
const MB = 1024 * 1024;
|
|
7
7
|
|
|
@@ -29,11 +29,6 @@ export function normalizePriority(value) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
function nonNegativeInt(value, fallback) {
|
|
33
|
-
const parsed = Math.floor(Number(value));
|
|
34
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
32
|
function envLimits(env = process.env) {
|
|
38
33
|
return {
|
|
39
34
|
maxAgents: positiveInt(env.MIXDOG_MAX_CONCURRENT_AGENTS, RESOURCE_ADMISSION_DEFAULTS.maxAgents),
|
|
@@ -12,16 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
|
|
15
|
+
import { createPgSchemaDb } from './pg-schema-db.mjs';
|
|
15
16
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
16
17
|
|
|
17
18
|
const SCHEMA = 'scheduler';
|
|
18
19
|
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Lazy connection + one-shot idempotent DDL (keyed per resolved dataDir).
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
const _ready = new Map(); // dataDir → Promise<db>
|
|
24
|
-
|
|
25
20
|
const DDL = `
|
|
26
21
|
CREATE TABLE IF NOT EXISTS scheduler.schedules (
|
|
27
22
|
name text PRIMARY KEY,
|
|
@@ -61,24 +56,13 @@ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_success_at timesta
|
|
|
61
56
|
ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_failed_at timestamptz;
|
|
62
57
|
`;
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await withSchemaBootstrapLock(pool, () => db.exec(DDL));
|
|
72
|
-
return db;
|
|
73
|
-
})();
|
|
74
|
-
_ready.set(dataDir, p);
|
|
75
|
-
try {
|
|
76
|
-
return await p;
|
|
77
|
-
} catch (err) {
|
|
78
|
-
_ready.delete(dataDir); // let the next call retry DDL after a transient failure
|
|
79
|
-
throw err;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
59
|
+
const getDb = createPgSchemaDb({
|
|
60
|
+
schema: SCHEMA,
|
|
61
|
+
ddl: DDL,
|
|
62
|
+
defaultDataDir: resolvePluginData,
|
|
63
|
+
ensurePg: ensurePgInstance,
|
|
64
|
+
withLock: withSchemaBootstrapLock,
|
|
65
|
+
});
|
|
82
66
|
|
|
83
67
|
// ---------------------------------------------------------------------------
|
|
84
68
|
// Row <-> def mapping
|