pilotswarm-sdk 0.5.47 → 0.5.49

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.
Files changed (48) hide show
  1. package/api/src/http-api-transport.js +11 -0
  2. package/api/src/protocol.js +1 -0
  3. package/dist/cms-migrations.d.ts.map +1 -1
  4. package/dist/cms-migrations.js +440 -0
  5. package/dist/cms-migrations.js.map +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/management-client.d.ts +87 -0
  11. package/dist/management-client.d.ts.map +1 -1
  12. package/dist/management-client.js +62 -5
  13. package/dist/management-client.js.map +1 -1
  14. package/dist/model-providers.d.ts +36 -5
  15. package/dist/model-providers.d.ts.map +1 -1
  16. package/dist/model-providers.js +41 -6
  17. package/dist/model-providers.js.map +1 -1
  18. package/dist/provider-catalog.d.ts.map +1 -1
  19. package/dist/provider-catalog.js +58 -10
  20. package/dist/provider-catalog.js.map +1 -1
  21. package/dist/provider-store.d.ts +45 -1
  22. package/dist/provider-store.d.ts.map +1 -1
  23. package/dist/provider-store.js +62 -2
  24. package/dist/provider-store.js.map +1 -1
  25. package/dist/provider-tools.d.ts +8 -0
  26. package/dist/provider-tools.d.ts.map +1 -1
  27. package/dist/provider-tools.js +39 -1
  28. package/dist/provider-tools.js.map +1 -1
  29. package/dist/session-manager.d.ts.map +1 -1
  30. package/dist/session-manager.js +28 -5
  31. package/dist/session-manager.js.map +1 -1
  32. package/dist/session-proxy.d.ts.map +1 -1
  33. package/dist/session-proxy.js +9 -5
  34. package/dist/session-proxy.js.map +1 -1
  35. package/dist/web/generated-op-methods.d.ts +8 -0
  36. package/dist/web/generated-op-methods.d.ts.map +1 -1
  37. package/dist/web/generated-op-methods.js +2 -0
  38. package/dist/web/generated-op-methods.js.map +1 -1
  39. package/dist/web/web-management-client.d.ts +4 -0
  40. package/dist/web/web-management-client.d.ts.map +1 -1
  41. package/dist/web/web-management-client.js +7 -0
  42. package/dist/web/web-management-client.js.map +1 -1
  43. package/dist/wif-credentials.d.ts +181 -0
  44. package/dist/wif-credentials.d.ts.map +1 -0
  45. package/dist/wif-credentials.js +382 -0
  46. package/dist/wif-credentials.js.map +1 -0
  47. package/package.json +3 -3
  48. package/plugins/mgmt/agents/token-manager.agent.md +1 -0
@@ -431,10 +431,21 @@ export class HttpApiTransport {
431
431
  listProviders() { return this.api.call("listProviders"); }
432
432
  getProviderStatus(names) { return this.api.call("getProviderStatus", { names: providerNamesParam(names) }); }
433
433
  getProviderUsageGrid() { return this.api.call("getProviderUsageGrid"); }
434
+ getProviderUsageSummary({ days, providers } = {}) {
435
+ // The wire carries the provider list as one comma-separated query
436
+ // value; the server splits it. Names never contain commas.
437
+ const list = Array.isArray(providers) ? providers.filter(Boolean).join(",") : (providers || undefined);
438
+ return this.api.call("getProviderUsageSummary", { ...(days ? { days } : {}), ...(list ? { providers: list } : {}) });
439
+ }
434
440
  createProvider({ name, type, credentials, baseUrl, displayName } = {}) { return this.api.call("createProvider", { name, type, credentials, baseUrl, displayName }); }
435
441
  deleteProvider(name) { return this.api.call("deleteProvider", { name }); }
436
442
  createMyProvider({ name, type, credentials, baseUrl, displayName } = {}) { return this.api.call("createMyProvider", { name, type, credentials, baseUrl, displayName }); }
437
443
  updateMyProviderCredential({ name, credentials } = {}) { return this.api.call("updateMyProviderCredential", { name, credentials }); }
444
+ // Admin-only; the server refuses everyone else. This is the browser's and
445
+ // the CLI's path to the shared rotation — 0.5.47 shipped the op on the
446
+ // HTTP table, the MCP and the agent tools but not here, so the portal's
447
+ // Update Key on a shared row reported "not available on this transport".
448
+ updateSharedProviderCredential({ name, credentials } = {}) { return this.api.call("updateSharedProviderCredential", { name, credentials }); }
438
449
  deleteMyProvider(name) { return this.api.call("deleteMyProvider", { name }); }
439
450
  clearProviderRoutingDependencies(name) { return this.api.call("clearProviderRoutingDependencies", { name }); }
440
451
  setProviderLimit({ name, period, model, tokens } = {}) { return this.api.call("setProviderLimit", { name, period, model: model ?? null, tokens }); }
@@ -273,6 +273,7 @@ export const OPERATIONS = [
273
273
  { name: "clearSystemSessionModel", access: "fleet:admin", method: "DELETE", path: "/management/system-sessions/:agentId/model", params: { agentId: path("agentId") }, summary: "Clear one persistent system-agent model override. [admin]" },
274
274
  { name: "setClusterDefault", access: "fleet:admin", method: "PUT", path: "/management/defaults", params: { provider: body(), model: body(), reasoning: body(), context: body() }, summary: "Deprecated alias for setModelDefault(scope=cluster). [admin]" },
275
275
  { name: "setMyDefault", access: "authed", method: "PUT", path: "/me/default", params: { provider: body(), model: body(), reasoning: body(), context: body() }, summary: "The caller's prefill for new sessions. A null provider clears it." },
276
+ { name: "getProviderUsageSummary", access: "authed", method: "GET", path: "/providers/usage-summary", params: { days: query("number"), providers: query("string") }, summary: "The cluster summary from the usage ledger: today / week / month token totals with the input, output and cache split, a per-UTC-day series, and the per-model pivot across providers, reasoning efforts and context tiers. `providers` is a comma-separated list of names; absent means all. Admins see the whole cluster (system sessions included); everyone else sees their own turns." },
276
277
  { name: "getProviderUsage", access: "authed", method: "GET", path: "/providers/usage", params: { days: query("number"), mine: query("boolean"), ownerUserId: query("number"), provider: query("string"), model: query("string"), sessionId: query("string"), chargeClass: query("string"), dimension: query("string"), limit: query("number") }, summary: "Where the tokens went: { totals, daily[], breakdown[] } over one filter set. dimension: session | user | provider | model | agent. Non-admins see only their own rows. `mine` narrows to the caller's own spend, resolved server-side — it carries no id, so it cannot name anybody else." },
277
278
  { name: "listPausedSessions", access: "authed", method: "GET", path: "/providers/paused", summary: "Sessions waiting on a limit, allowance, hold, or a provider name that no longer resolves. Admins fleet-wide, everyone else their own." },
278
279
 
@@ -1 +1 @@
1
- {"version":3,"file":"cms-migrations.d.ts","sourceRoot":"","sources":["../src/cms-migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAkV/D"}
1
+ {"version":3,"file":"cms-migrations.d.ts","sourceRoot":"","sources":["../src/cms-migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAiW/D"}
@@ -347,6 +347,21 @@ export function CMS_MIGRATIONS(schema) {
347
347
  name: "shared_provider_credential_update",
348
348
  sql: migration_0067_shared_provider_credential_update(schema),
349
349
  },
350
+ {
351
+ version: "0068",
352
+ name: "provider_usage_summary",
353
+ sql: migration_0068_provider_usage_summary(schema),
354
+ },
355
+ {
356
+ version: "0069",
357
+ name: "provider_grid_token_split",
358
+ sql: migration_0069_provider_grid_token_split(schema),
359
+ },
360
+ {
361
+ version: "0070",
362
+ name: "token_total_is_input_plus_output",
363
+ steps: migration_0070_token_total_is_input_plus_output(schema),
364
+ },
350
365
  ];
351
366
  }
352
367
  // ─── Migration 0044: ms-space keyset for session paging ──────────
@@ -13768,4 +13783,429 @@ END;
13768
13783
  $$ LANGUAGE plpgsql VOLATILE;
13769
13784
  `;
13770
13785
  }
13786
+ /**
13787
+ * 0068: the cluster summary — every provider at once, pivoted by model.
13788
+ *
13789
+ * The Providers tab answers "how much of THIS provider's limit is used",
13790
+ * one provider at a time, from the meters. This answers the other question:
13791
+ * how many tokens the cluster burned today / this week / this month, per
13792
+ * UTC day for a chart, and per MODEL — folded across providers, reasoning
13793
+ * efforts and context tiers, because a person planning capacity thinks in
13794
+ * models, not in the routes tokens took to reach them.
13795
+ *
13796
+ * Read from the ledger, not the meters. The meters count only 'user'
13797
+ * charges (system sessions are exempt from limits by design), so a summary
13798
+ * built on them would silently omit the sweeper, the token manager and
13799
+ * every other system agent — on a real cluster the larger half. The ledger
13800
+ * has every turn; `classes` says how the total splits so nobody has to
13801
+ * wonder why this number is bigger than the Providers tab's.
13802
+ *
13803
+ * Scope: an admin sees the cluster; anyone else sees their own turns. The
13804
+ * provider filter is a plain list of names — the portal turns "Shared" and
13805
+ * "Users" into names before asking, so the database never has to guess
13806
+ * what a preset meant.
13807
+ */
13808
+ function migration_0068_provider_usage_summary(schema) {
13809
+ const s = schema;
13810
+ return `
13811
+ -- The rows every aggregate below reads: one place for the scope and the
13812
+ -- provider filter, so the KPIs, the chart and the model table can never
13813
+ -- disagree about which turns were counted.
13814
+ CREATE OR REPLACE FUNCTION ${s}.cms_provider_usage_summary_rows(
13815
+ p_viewer BIGINT, p_is_admin BOOLEAN, p_since TIMESTAMPTZ, p_providers TEXT[]
13816
+ ) RETURNS SETOF ${s}.provider_usage_ledger AS $$
13817
+ SELECT l.*
13818
+ FROM ${s}.provider_usage_ledger l
13819
+ WHERE l.created_at >= p_since
13820
+ AND (COALESCE(p_is_admin, FALSE) OR l.owner_user_id IS NOT DISTINCT FROM p_viewer)
13821
+ AND (p_providers IS NULL OR cardinality(p_providers) = 0 OR l.provider_name = ANY(p_providers));
13822
+ $$ LANGUAGE sql STABLE;
13823
+
13824
+ CREATE OR REPLACE FUNCTION ${s}.cms_provider_usage_summary_window(
13825
+ p_viewer BIGINT, p_is_admin BOOLEAN, p_since TIMESTAMPTZ, p_providers TEXT[]
13826
+ ) RETURNS JSONB AS $$
13827
+ SELECT jsonb_build_object(
13828
+ 'input', COALESCE(sum(r.tokens_input), 0),
13829
+ 'output', COALESCE(sum(r.tokens_output), 0),
13830
+ 'cacheRead', COALESCE(sum(r.tokens_cache_read), 0),
13831
+ 'cacheWrite', COALESCE(sum(r.tokens_cache_write), 0),
13832
+ 'total', COALESCE(sum(r.tokens_total), 0),
13833
+ 'turns', count(*),
13834
+ 'sessions', count(DISTINCT r.session_id))
13835
+ FROM ${s}.cms_provider_usage_summary_rows(p_viewer, p_is_admin, p_since, p_providers) r;
13836
+ $$ LANGUAGE sql STABLE;
13837
+
13838
+ CREATE OR REPLACE FUNCTION ${s}.cms_provider_usage_summary(
13839
+ p_viewer BIGINT, p_is_admin BOOLEAN, p_days INTEGER, p_providers TEXT[]
13840
+ ) RETURNS JSONB AS $$
13841
+ DECLARE
13842
+ v_days INTEGER := LEAST(GREATEST(COALESCE(p_days, 14), 1), 365);
13843
+ -- UTC days, today included: the ledger stores UTC and the meters' day /
13844
+ -- week / month windows are UTC, so "today" here is the same today.
13845
+ v_today DATE := (now() AT TIME ZONE 'UTC')::date;
13846
+ v_from TIMESTAMPTZ := ((v_today - (v_days - 1)) :: timestamp) AT TIME ZONE 'UTC';
13847
+ v_month TIMESTAMPTZ := ((v_today - 29) :: timestamp) AT TIME ZONE 'UTC';
13848
+ v_since TIMESTAMPTZ := LEAST(v_from, v_month);
13849
+ v_windows JSONB;
13850
+ v_daily JSONB;
13851
+ v_models JSONB;
13852
+ v_classes JSONB;
13853
+ BEGIN
13854
+ -- Today / last 7 UTC days / last 30 UTC days, each with the four-way
13855
+ -- split. The month window is what the ledger is read for at minimum.
13856
+ SELECT jsonb_build_object(
13857
+ 'day', ${s}.cms_provider_usage_summary_window(p_viewer, p_is_admin, ((v_today)::timestamp) AT TIME ZONE 'UTC', p_providers),
13858
+ 'week', ${s}.cms_provider_usage_summary_window(p_viewer, p_is_admin, ((v_today - 6)::timestamp) AT TIME ZONE 'UTC', p_providers),
13859
+ 'month', ${s}.cms_provider_usage_summary_window(p_viewer, p_is_admin, v_month, p_providers))
13860
+ INTO v_windows;
13861
+
13862
+ SELECT COALESCE(jsonb_agg(jsonb_build_object(
13863
+ 'day', to_char(d.day, 'YYYY-MM-DD'),
13864
+ 'input', d.i, 'output', d.o, 'cacheRead', d.cr, 'cacheWrite', d.cw,
13865
+ 'total', d.t, 'turns', d.n) ORDER BY d.day), '[]'::jsonb)
13866
+ INTO v_daily
13867
+ FROM (
13868
+ SELECT (r.created_at AT TIME ZONE 'UTC')::date AS day,
13869
+ sum(r.tokens_input) i, sum(r.tokens_output) o,
13870
+ sum(r.tokens_cache_read) cr, sum(r.tokens_cache_write) cw,
13871
+ sum(r.tokens_total) t, count(*) n
13872
+ FROM ${s}.cms_provider_usage_summary_rows(p_viewer, p_is_admin, v_from, p_providers) r
13873
+ GROUP BY 1) d;
13874
+
13875
+ -- The pivot: the model NAME, whatever provider carried it. model_qualified
13876
+ -- is provider:model, and a model name never holds a colon, so the part
13877
+ -- after the first colon is the model. Each row carries its own per-day
13878
+ -- totals for a sparkline.
13879
+ SELECT COALESCE(jsonb_agg(jsonb_build_object(
13880
+ 'model', m.model, 'providers', m.providers, 'turns', m.n,
13881
+ 'input', m.i, 'output', m.o, 'cacheRead', m.cr, 'cacheWrite', m.cw, 'total', m.t,
13882
+ 'daily', m.daily) ORDER BY m.t DESC, m.model), '[]'::jsonb)
13883
+ INTO v_models
13884
+ FROM (
13885
+ SELECT g.model, count(DISTINCT g.provider_name) providers, count(*) n,
13886
+ sum(g.tokens_input) i, sum(g.tokens_output) o,
13887
+ sum(g.tokens_cache_read) cr, sum(g.tokens_cache_write) cw, sum(g.tokens_total) t,
13888
+ (SELECT COALESCE(jsonb_agg(jsonb_build_object('day', to_char(dd.day, 'YYYY-MM-DD'), 'total', dd.t) ORDER BY dd.day), '[]'::jsonb)
13889
+ FROM (SELECT (x.created_at AT TIME ZONE 'UTC')::date AS day, sum(x.tokens_total) t
13890
+ FROM ${s}.cms_provider_usage_summary_rows(p_viewer, p_is_admin, v_from, p_providers) x
13891
+ WHERE substr(x.model_qualified, position(':' IN x.model_qualified) + 1) = g.model
13892
+ GROUP BY 1) dd) AS daily
13893
+ FROM (SELECT r.*, substr(r.model_qualified, position(':' IN r.model_qualified) + 1) AS model
13894
+ FROM ${s}.cms_provider_usage_summary_rows(p_viewer, p_is_admin, v_from, p_providers) r) g
13895
+ GROUP BY g.model) m;
13896
+
13897
+ -- How the window's total splits by who was charged: people, the
13898
+ -- machinery PilotSwarm runs for itself, or turns with no provider.
13899
+ SELECT COALESCE(jsonb_agg(jsonb_build_object('chargeClass', c.charge_class, 'total', c.t, 'turns', c.n) ORDER BY c.t DESC), '[]'::jsonb)
13900
+ INTO v_classes
13901
+ FROM (SELECT r.charge_class, sum(r.tokens_total) t, count(*) n
13902
+ FROM ${s}.cms_provider_usage_summary_rows(p_viewer, p_is_admin, v_from, p_providers) r
13903
+ GROUP BY 1) c;
13904
+
13905
+ RETURN jsonb_build_object(
13906
+ 'days', v_days,
13907
+ 'today', to_char(v_today, 'YYYY-MM-DD'),
13908
+ 'scope', CASE WHEN COALESCE(p_is_admin, FALSE) THEN 'cluster' ELSE 'mine' END,
13909
+ 'windows', v_windows,
13910
+ 'daily', v_daily,
13911
+ 'models', v_models,
13912
+ 'classes', v_classes);
13913
+ END;
13914
+ $$ LANGUAGE plpgsql STABLE;
13915
+
13916
+ `;
13917
+ }
13918
+ /**
13919
+ * 0069: the four-way token split on the Providers page.
13920
+ *
13921
+ * The grid's period cells carried one number per pair (used / quota, yours /
13922
+ * your share) because the METERS hold one number: a limit is on the total,
13923
+ * and the total is input + output + cache read + cache write
13924
+ * (cms_provider_settle_turn, v_total). What went into that total was only
13925
+ * visible on the Cluster summary. Each cell now also carries the split, for
13926
+ * everyone and for the viewer, read from the ledger over the SAME window
13927
+ * and scope the meter covers and over the same 'user' turns the meter
13928
+ * counts — so the four parts add up to the number beside them.
13929
+ *
13930
+ * The per-day series under a selected provider gets the same split, which
13931
+ * widens its RETURNS TABLE, hence DROP + CREATE with the same arguments.
13932
+ */
13933
+ function migration_0069_provider_grid_token_split(schema) {
13934
+ const s = schema;
13935
+ return `
13936
+ CREATE OR REPLACE FUNCTION ${s}.cms_provider_usage_grid(
13937
+ p_viewer BIGINT, p_is_admin BOOLEAN
13938
+ ) RETURNS TABLE(
13939
+ provider_name TEXT, row_kind TEXT, scope TEXT, class TEXT,
13940
+ allowance_pct SMALLINT, hold_until_utc TIMESTAMPTZ, hold_indefinite BOOLEAN,
13941
+ model_row_count INTEGER, owned_by_me BOOLEAN, manageable BOOLEAN,
13942
+ owner_label TEXT, periods JSONB
13943
+ ) AS $$
13944
+ WITH windows AS (
13945
+ SELECT per.period, wb.window_start, wb.resets_at, wb.window_key
13946
+ FROM (VALUES ('day'),('week'),('month')) AS per(period)
13947
+ CROSS JOIN LATERAL ${s}.cms_provider_window_bounds(per.period, now()) wb
13948
+ ),
13949
+ model_scopes AS (
13950
+ SELECT r.provider_name, r.model_qualified AS scope
13951
+ FROM ${s}.provider_budget_rules r
13952
+ WHERE r.model_qualified IS NOT NULL
13953
+ UNION
13954
+ SELECT m.provider_name, m.scope
13955
+ FROM ${s}.provider_meters m
13956
+ JOIN windows w ON w.period = m.period AND w.window_key = m.window_key_utc
13957
+ WHERE m.scope <> '*'
13958
+ ),
13959
+ visible AS (
13960
+ SELECT pi.name, pi.class, pi.allowance_pct, pi.hold_until_utc, pi.hold_indefinite,
13961
+ pi.created_at AS named_at,
13962
+ (pi.class = 'shared' OR pi.owner_user_id IS NOT DISTINCT FROM p_viewer) AS owned_by_me,
13963
+ (CASE WHEN pi.class = 'shared' THEN COALESCE(p_is_admin, FALSE)
13964
+ ELSE pi.owner_user_id IS NOT DISTINCT FROM p_viewer END) AS manageable,
13965
+ (CASE WHEN pi.class = 'shared' OR pi.owner_user_id IS NULL THEN NULL
13966
+ ELSE COALESCE(
13967
+ NULLIF(BTRIM(u.display_name), ''),
13968
+ NULLIF(BTRIM(u.email), ''),
13969
+ 'user ' || pi.owner_user_id::text) END) AS owner_label,
13970
+ (SELECT count(*)::INTEGER FROM model_scopes ms WHERE ms.provider_name = pi.name) AS model_rows
13971
+ FROM ${s}.provider_instances pi
13972
+ LEFT JOIN ${s}.users u ON u.user_id = pi.owner_user_id
13973
+ WHERE pi.class = 'shared' OR pi.owner_user_id = p_viewer OR COALESCE(p_is_admin, FALSE)
13974
+ ),
13975
+ grid_rows AS (
13976
+ SELECT v.*, 'provider'::TEXT AS row_kind, '*'::TEXT AS scope FROM visible v
13977
+ UNION ALL
13978
+ SELECT v.*, 'model'::TEXT, ms.scope
13979
+ FROM visible v
13980
+ JOIN model_scopes ms ON ms.provider_name = v.name
13981
+ )
13982
+ SELECT g.name, g.row_kind, g.scope, g.class, g.allowance_pct,
13983
+ g.hold_until_utc, g.hold_indefinite,
13984
+ CASE WHEN g.row_kind = 'provider' THEN g.model_rows ELSE 0 END,
13985
+ g.owned_by_me, g.manageable,
13986
+ CASE WHEN g.row_kind = 'provider' THEN g.owner_label ELSE NULL END,
13987
+ jsonb_object_agg(w.period, jsonb_build_object(
13988
+ 'ruleId', r.rule_id,
13989
+ 'quotaTokens', r.limit_tokens,
13990
+ 'usedTokens', COALESCE(m.used_tokens, 0),
13991
+ 'yourQuotaTokens', CASE
13992
+ WHEN r.limit_tokens IS NULL OR p_viewer IS NULL THEN NULL
13993
+ WHEN NOT g.owned_by_me THEN NULL
13994
+ WHEN g.allowance_pct < 100
13995
+ THEN ${s}.cms_provider_ceiling(r.limit_tokens, g.allowance_pct)
13996
+ ELSE r.limit_tokens END,
13997
+ 'yourUsedTokens', CASE WHEN p_viewer IS NULL THEN NULL
13998
+ ELSE COALESCE(mu.used_tokens, 0) END,
13999
+ -- What the used figure is made of. Same window, same scope,
14000
+ -- same 'user' turns as the meter, so the four add up to it.
14001
+ 'inputTokens', COALESCE(ls.i, 0),
14002
+ 'outputTokens', COALESCE(ls.o, 0),
14003
+ 'cacheReadTokens', COALESCE(ls.cr, 0),
14004
+ 'cacheWriteTokens', COALESCE(ls.cw, 0),
14005
+ 'yourInputTokens', CASE WHEN p_viewer IS NULL THEN NULL ELSE COALESCE(lu.i, 0) END,
14006
+ 'yourOutputTokens', CASE WHEN p_viewer IS NULL THEN NULL ELSE COALESCE(lu.o, 0) END,
14007
+ 'yourCacheReadTokens', CASE WHEN p_viewer IS NULL THEN NULL ELSE COALESCE(lu.cr, 0) END,
14008
+ 'yourCacheWriteTokens', CASE WHEN p_viewer IS NULL THEN NULL ELSE COALESCE(lu.cw, 0) END,
14009
+ 'windowStartUtc', to_char(w.window_start AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
14010
+ 'resetsAtUtc', to_char(w.resets_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')))
14011
+ FROM grid_rows g
14012
+ CROSS JOIN windows w
14013
+ LEFT JOIN ${s}.provider_budget_rules r
14014
+ ON r.provider_name = g.name AND r.period = w.period
14015
+ AND COALESCE(r.model_qualified, '*') = g.scope
14016
+ LEFT JOIN ${s}.provider_meters m
14017
+ ON m.provider_name = g.name AND m.period = w.period
14018
+ AND m.scope = g.scope AND m.window_key_utc = w.window_key
14019
+ LEFT JOIN ${s}.provider_meters_user mu
14020
+ ON mu.provider_name = g.name AND mu.period = w.period
14021
+ AND mu.scope = g.scope AND mu.window_key_utc = w.window_key
14022
+ AND mu.user_id = p_viewer
14023
+ LEFT JOIN LATERAL (
14024
+ SELECT sum(l.tokens_input) i, sum(l.tokens_output) o,
14025
+ sum(l.tokens_cache_read) cr, sum(l.tokens_cache_write) cw
14026
+ FROM ${s}.provider_usage_ledger l
14027
+ WHERE l.provider_name = g.name AND l.charge_class = 'user'
14028
+ AND l.created_at >= GREATEST(w.window_start, g.named_at) AND l.created_at < w.resets_at
14029
+ AND (g.scope = '*' OR l.model_qualified = g.scope)) ls ON TRUE
14030
+ LEFT JOIN LATERAL (
14031
+ SELECT sum(l.tokens_input) i, sum(l.tokens_output) o,
14032
+ sum(l.tokens_cache_read) cr, sum(l.tokens_cache_write) cw
14033
+ FROM ${s}.provider_usage_ledger l
14034
+ WHERE l.provider_name = g.name AND l.charge_class = 'user'
14035
+ AND l.owner_user_id = p_viewer
14036
+ AND l.created_at >= GREATEST(w.window_start, g.named_at) AND l.created_at < w.resets_at
14037
+ AND (g.scope = '*' OR l.model_qualified = g.scope)) lu ON TRUE
14038
+ GROUP BY g.name, g.row_kind, g.scope, g.class, g.allowance_pct,
14039
+ g.hold_until_utc, g.hold_indefinite, g.model_rows,
14040
+ g.owned_by_me, g.manageable, g.owner_label
14041
+ ORDER BY (g.class = 'shared') DESC, g.name, (g.row_kind = 'model'), g.scope;
14042
+ $$ LANGUAGE sql STABLE;
14043
+
14044
+ DROP FUNCTION IF EXISTS ${s}.cms_provider_usage_daily(BIGINT, BOOLEAN, INTEGER, BIGINT, TEXT, TEXT, TEXT, TEXT);
14045
+
14046
+ CREATE FUNCTION ${s}.cms_provider_usage_daily(
14047
+ p_viewer BIGINT, p_is_admin BOOLEAN, p_days INTEGER,
14048
+ p_owner BIGINT, p_provider TEXT, p_model TEXT, p_session TEXT, p_class TEXT
14049
+ ) RETURNS TABLE(
14050
+ day_utc DATE, tokens_total BIGINT, turns BIGINT,
14051
+ tokens_input BIGINT, tokens_output BIGINT, tokens_cache_read BIGINT, tokens_cache_write BIGINT
14052
+ ) AS $$
14053
+ SELECT (l.created_at AT TIME ZONE 'UTC')::date, COALESCE(sum(l.tokens_total), 0), count(*)::BIGINT,
14054
+ COALESCE(sum(l.tokens_input), 0), COALESCE(sum(l.tokens_output), 0),
14055
+ COALESCE(sum(l.tokens_cache_read), 0), COALESCE(sum(l.tokens_cache_write), 0)
14056
+ FROM ${s}.provider_usage_ledger l
14057
+ WHERE l.created_at >= now() - (COALESCE(p_days, 7) || ' days')::interval
14058
+ AND (COALESCE(p_is_admin, FALSE)
14059
+ OR l.owner_user_id IS NOT DISTINCT FROM p_viewer
14060
+ OR (p_provider IS NOT NULL AND p_owner IS NULL AND p_session IS NULL
14061
+ AND EXISTS (
14062
+ SELECT 1 FROM ${s}.provider_instances pi
14063
+ WHERE pi.name = p_provider AND pi.class = 'shared')))
14064
+ AND (p_owner IS NULL OR l.owner_user_id = p_owner)
14065
+ AND (p_provider IS NULL OR l.provider_name = p_provider)
14066
+ AND (p_provider IS NULL OR l.created_at >= COALESCE(
14067
+ (SELECT pi.created_at FROM ${s}.provider_instances pi WHERE pi.name = p_provider),
14068
+ '-infinity'::timestamptz))
14069
+ AND (p_model IS NULL OR l.model_qualified = p_model)
14070
+ AND (p_session IS NULL OR l.session_id = p_session)
14071
+ AND (p_class IS NULL OR l.charge_class = p_class)
14072
+ GROUP BY 1 ORDER BY 1;
14073
+ $$ LANGUAGE sql STABLE;
14074
+ `;
14075
+ }
14076
+ /**
14077
+ * 0070: a turn's total is input + output. Cache reads and writes are parts
14078
+ * of the input, not additions to it.
14079
+ *
14080
+ * `buildUsageSummaryUpsert` (session-proxy.ts) stores the INCLUSIVE prompt
14081
+ * count: `tokens_input` already contains what was served from the cache and
14082
+ * what was written to it. The ledger and the meters proved it — in every row
14083
+ * on the local database and on chk, across GPT, Claude, Grok and MAI,
14084
+ * `tokens_cache_read + tokens_cache_write <= tokens_input`, and when a turn
14085
+ * wrote to the cache the two sides were equal. Yet `cms_provider_settle_turn`
14086
+ * summed all four into `tokens_total`, so every cached prompt was charged
14087
+ * twice: chk's last 30 days read 3.62B tokens for 1.97B consumed. That
14088
+ * total is what the meters hold and what every limit is compared against,
14089
+ * so quotas were biting at roughly half their stated size.
14090
+ *
14091
+ * Three things, as separate steps: the function, the ledger backfill, and
14092
+ * the meters rebuilt from the corrected ledger for their live windows (the
14093
+ * meters are a cache of the ledger; the reconciliation that showed them
14094
+ * exact is the same query, so nothing here invents a number). After this,
14095
+ * every "used" figure drops by the cached amount — limits that were tuned
14096
+ * against the inflated figures are looser in practice from here on.
14097
+ */
14098
+ function migration_0070_token_total_is_input_plus_output(schema) {
14099
+ const s = schema;
14100
+ return [
14101
+ `
14102
+ CREATE OR REPLACE FUNCTION ${s}.cms_provider_settle_turn(
14103
+ p_session_id TEXT, p_turn_index INTEGER, p_provider TEXT, p_model TEXT,
14104
+ p_owner BIGINT, p_charge_class TEXT, p_agent_id TEXT,
14105
+ p_in BIGINT, p_out BIGINT, p_cache_read BIGINT, p_cache_write BIGINT
14106
+ ) RETURNS BOOLEAN AS $$
14107
+ DECLARE
14108
+ -- input + output. The cache figures are NOT added: as the Copilot SDK
14109
+ -- reports usage, cache_read and cache_write are parts OF the input
14110
+ -- (cache_read + cache_write <= input in every row ever recorded, and
14111
+ -- equal to it when writes occur), so adding them again charged a
14112
+ -- cached prompt twice — chk was over-counting by 84% (0070).
14113
+ v_total BIGINT := COALESCE(p_in,0) + COALESCE(p_out,0);
14114
+ v_class TEXT := COALESCE(NULLIF(BTRIM(COALESCE(p_charge_class,'')), ''), 'user');
14115
+ v_scope TEXT := COALESCE(NULLIF(BTRIM(COALESCE(p_model,'')), ''), '*');
14116
+ -- The lifetime offset. Stable for the whole turn: only a restart moves
14117
+ -- it, and a restart deletes the orchestration mid-turn anyway.
14118
+ v_index INTEGER := COALESCE(p_turn_index, 0) + COALESCE(
14119
+ (SELECT ss.provider_ledger_base FROM ${s}.sessions ss
14120
+ WHERE ss.session_id = p_session_id), 0);
14121
+ v_first INTEGER;
14122
+ BEGIN
14123
+ IF p_provider IS NULL THEN v_class := 'unattributed'; END IF;
14124
+
14125
+ INSERT INTO ${s}.provider_usage_ledger
14126
+ (session_id, turn_index, provider_name, model_qualified, owner_user_id,
14127
+ charge_class, tokens_input, tokens_output, tokens_cache_read,
14128
+ tokens_cache_write, tokens_total, agent_id)
14129
+ VALUES (p_session_id, v_index, p_provider, p_model, p_owner,
14130
+ v_class, COALESCE(p_in,0), COALESCE(p_out,0), COALESCE(p_cache_read,0),
14131
+ COALESCE(p_cache_write,0), v_total, p_agent_id)
14132
+ ON CONFLICT (session_id, turn_index) DO NOTHING;
14133
+ GET DIAGNOSTICS v_first = ROW_COUNT;
14134
+ IF v_first = 0 THEN RETURN FALSE; END IF;
14135
+
14136
+ IF v_class <> 'user' OR p_provider IS NULL OR v_total <= 0 THEN
14137
+ RETURN TRUE;
14138
+ END IF;
14139
+
14140
+ PERFORM 1 FROM ${s}.provider_instances pi
14141
+ WHERE pi.name = p_provider FOR KEY SHARE;
14142
+ IF NOT FOUND THEN RETURN TRUE; END IF;
14143
+
14144
+ INSERT INTO ${s}.provider_meters
14145
+ (provider_name, period, scope, window_key_utc, used_tokens,
14146
+ window_start_utc, resets_at_utc)
14147
+ SELECT p_provider, per.period, sc.scope, wb.window_key, v_total,
14148
+ wb.window_start, wb.resets_at
14149
+ FROM (VALUES ('day'),('week'),('month')) AS per(period)
14150
+ CROSS JOIN LATERAL ${s}.cms_provider_window_bounds(per.period, now()) wb
14151
+ CROSS JOIN (SELECT DISTINCT v.s FROM (VALUES ('*'), (v_scope)) AS v(s)) AS sc(scope)
14152
+ ORDER BY per.period, sc.scope
14153
+ ON CONFLICT (provider_name, period, scope, window_key_utc) DO UPDATE
14154
+ SET used_tokens = ${s}.provider_meters.used_tokens + EXCLUDED.used_tokens,
14155
+ updated_at = now();
14156
+
14157
+ IF p_owner IS NOT NULL THEN
14158
+ INSERT INTO ${s}.provider_meters_user
14159
+ (provider_name, period, scope, window_key_utc, user_id, used_tokens,
14160
+ window_start_utc, resets_at_utc)
14161
+ SELECT p_provider, per.period, sc.scope, wb.window_key, p_owner, v_total,
14162
+ wb.window_start, wb.resets_at
14163
+ FROM (VALUES ('day'),('week'),('month')) AS per(period)
14164
+ CROSS JOIN LATERAL ${s}.cms_provider_window_bounds(per.period, now()) wb
14165
+ CROSS JOIN (SELECT DISTINCT v.s FROM (VALUES ('*'), (v_scope)) AS v(s)) AS sc(scope)
14166
+ ORDER BY per.period, sc.scope
14167
+ ON CONFLICT (provider_name, period, scope, window_key_utc, user_id) DO UPDATE
14168
+ SET used_tokens = ${s}.provider_meters_user.used_tokens + EXCLUDED.used_tokens,
14169
+ updated_at = now();
14170
+ END IF;
14171
+
14172
+ RETURN TRUE;
14173
+ END;
14174
+ $$ LANGUAGE plpgsql VOLATILE;
14175
+ `,
14176
+ `
14177
+ SET lock_timeout = '5s';
14178
+ UPDATE ${s}.provider_usage_ledger
14179
+ SET tokens_total = tokens_input + tokens_output
14180
+ WHERE tokens_total IS DISTINCT FROM tokens_input + tokens_output;
14181
+ `,
14182
+ `
14183
+ UPDATE ${s}.provider_meters m
14184
+ SET used_tokens = COALESCE((
14185
+ SELECT sum(l.tokens_total)
14186
+ FROM ${s}.provider_usage_ledger l
14187
+ WHERE l.provider_name = m.provider_name
14188
+ AND l.charge_class = 'user'
14189
+ AND l.created_at >= GREATEST(m.window_start_utc,
14190
+ COALESCE((SELECT pi.created_at FROM ${s}.provider_instances pi WHERE pi.name = m.provider_name), '-infinity'::timestamptz))
14191
+ AND l.created_at < m.resets_at_utc
14192
+ AND (m.scope = '*' OR l.model_qualified = m.scope)), 0),
14193
+ updated_at = now();
14194
+ `,
14195
+ `
14196
+ UPDATE ${s}.provider_meters_user m
14197
+ SET used_tokens = COALESCE((
14198
+ SELECT sum(l.tokens_total)
14199
+ FROM ${s}.provider_usage_ledger l
14200
+ WHERE l.provider_name = m.provider_name
14201
+ AND l.charge_class = 'user'
14202
+ AND l.owner_user_id = m.user_id
14203
+ AND l.created_at >= GREATEST(m.window_start_utc,
14204
+ COALESCE((SELECT pi.created_at FROM ${s}.provider_instances pi WHERE pi.name = m.provider_name), '-infinity'::timestamptz))
14205
+ AND l.created_at < m.resets_at_utc
14206
+ AND (m.scope = '*' OR l.model_qualified = m.scope)), 0),
14207
+ updated_at = now();
14208
+ `,
14209
+ ];
14210
+ }
13771
14211
  //# sourceMappingURL=cms-migrations.js.map