@north-light/cli 0.3.550 → 0.3.552
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/dist/api/client.d.ts +11 -0
- package/dist/api/client.js +3 -3
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/charges.d.ts +122 -0
- package/dist/api/dto/charges.js +0 -0
- package/dist/api/dto/usage.d.ts +3 -3
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +3 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +10 -0
- package/dist/clients/attach/viewer.js +50 -50
- package/dist/commands/sys/spend.d.ts +1 -0
- package/dist/commands/sys/spend.js +3 -0
- package/dist/commands/sys.js +1 -1
- package/dist/core/canvas/canvas.js +4 -4
- package/dist/core/canvas/export-sqlite.js +2 -2
- package/dist/core/canvas/import-sqlite.js +1 -1
- package/dist/core/canvas/person.js +7 -16
- package/dist/core/canvas/schema-pg.sql +61 -0
- package/dist/core/canvas/types.d.ts +4 -0
- package/dist/core/config.js +1 -1
- package/dist/core/grants/mirror.d.ts +2 -1
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/ledger/backfill.d.ts +12 -0
- package/dist/core/ledger/backfill.js +22 -0
- package/dist/core/ledger/charges.d.ts +18 -0
- package/dist/core/ledger/charges.js +1 -0
- package/dist/core/ledger/check.d.ts +44 -0
- package/dist/core/ledger/check.js +7 -0
- package/dist/core/ledger/index.d.ts +9 -0
- package/dist/core/ledger/index.js +1 -0
- package/dist/core/ledger/limits.d.ts +15 -0
- package/dist/core/ledger/limits.js +3 -0
- package/dist/core/ledger/months.d.ts +6 -0
- package/dist/core/ledger/months.js +1 -0
- package/dist/core/ledger/payer.d.ts +34 -0
- package/dist/core/ledger/payer.js +1 -0
- package/dist/core/ledger/read.d.ts +44 -0
- package/dist/core/ledger/read.js +3 -0
- package/dist/core/ledger/totals.d.ts +19 -0
- package/dist/core/ledger/totals.js +4 -0
- package/dist/core/ledger/types.d.ts +66 -0
- package/dist/core/ledger/types.js +0 -0
- package/dist/core/llm/run.d.ts +17 -5
- package/dist/core/llm/run.js +2 -2
- package/dist/core/providers/call.d.ts +7 -1
- package/dist/core/providers/call.js +1 -1
- package/dist/core/providers/errors.js +1 -1
- package/dist/core/providers/receipts.d.ts +7 -1
- package/dist/core/providers/receipts.js +1 -3
- package/dist/core/runs/operations.d.ts +1 -0
- package/dist/core/runs/operations.js +7 -7
- package/dist/core/runtime/broker/cli-relay.js +1 -1
- package/dist/core/runtime/broker/model-adapter.js +2 -2
- package/dist/core/runtime/broker/turn-cause.d.ts +7 -0
- package/dist/core/runtime/broker/turn-cause.js +1 -0
- package/dist/core/runtime/broker.js +1 -1
- package/dist/core/runtime/nodes.d.ts +2 -0
- package/dist/core/runtime/nodes.js +1 -1
- package/dist/core/runtime/recycle.js +1 -1
- package/dist/core/runtime/reset.js +1 -1
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/spawn.d.ts +2 -0
- package/dist/core/runtime/spawn.js +2 -2
- package/dist/core/runtime/start-check.d.ts +15 -0
- package/dist/core/runtime/start-check.js +1 -0
- package/dist/core/runtime/warm-pool.js +1 -1
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/app-listener.js +13 -13
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/charges.d.ts +3 -0
- package/dist/daemon/api/handlers/charges.js +1 -0
- package/dist/daemon/api/handlers/llm.d.ts +9 -5
- package/dist/daemon/api/handlers/llm.js +3 -3
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/providers.d.ts +2 -0
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/api/handlers/run-routes.js +1 -1
- package/dist/daemon/api/handlers/runs.d.ts +1 -1
- package/dist/daemon/api/handlers/runs.js +1 -1
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/api/router.d.ts +8 -0
- package/dist/daemon/api/router.js +1 -1
- package/dist/daemon/api/server.js +1 -1
- package/dist/daemon/cron/authority.d.ts +1 -1
- package/dist/daemon/cron-run.js +2 -2
- package/dist/daemon/crtrd.js +10 -10
- package/dist/daemon/fleet.js +4 -4
- package/dist/daemon/reconcilers/action-lane.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +2 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
- package/dist/doc-editor/editor.css +1 -1
- package/dist/doc-editor/editor.js +16 -16
- package/dist/types.d.ts +4 -0
- package/docs/sdk/release-notes.md +10 -0
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -13
- package/dist/core/usage/ledger.d.ts +0 -49
- package/dist/core/usage/ledger.js +0 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var m=Object.defineProperty;var t=(e,r)=>m(e,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{parseScope as T}from"@north-light/identity";import{openDb as
|
|
1
|
+
var m=Object.defineProperty;var t=(e,r)=>m(e,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{parseScope as T}from"@north-light/identity";import{openDb as p,withCanvasWrite as w}from"../canvas/db.js";import{isUnsandboxedMachine as O}from"../runtime/unsandboxed.js";import{replaceWrittenBy as v,userSubject as R}from"../permissions/facts.js";import{GROUP_TYPE as x}from"../permissions/builtin.js";import{replaceGrantLimits as A}from"../ledger/index.js";const N=["not_started","pending","ready","failed","unavailable"],h=["ultra","strong","medium","light"];function u(e){if(!Array.isArray(e))throw new TypeError("grant scopes must be a list");return e.map(r=>{if(!r||typeof r!="object"||typeof r.scope!="string"||!["always","ask"].includes(r.mode))throw new TypeError("invalid grant scope entry");return{scope:r.scope,mode:r.mode}})}t(u,"entries");function L(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:r,message:n,next:i,step:o}=e;return typeof r!="string"||!N.includes(r)||n!==void 0&&typeof n!="string"||o!==void 0&&typeof o!="string"||typeof i!="string"||!i?null:{state:r,...n===void 0?{}:{message:n},next:i,...o===void 0?{}:{step:o}}}t(L,"dependencyOf");function E(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r={};for(const[n,i]of Object.entries(e)){const o=L(i);o&&(r[n]=o)}return r}t(E,"dependenciesOf");function b(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{base_url:r,rungs:n}=e;if(typeof r!="string"||!r||!n||typeof n!="object"||Array.isArray(n))return null;const i={};for(const o of h){const s=n[o];typeof s=="string"&&s&&(i[o]=s)}return Object.keys(i).length?{base_url:r.replace(/\/+$/,""),rungs:i}:null}t(b,"inferenceEndpointOf");function l(e){return{id:e.id,runtime:e.runtime,sub:e.sub,grantee:e.grantee,rev:e.rev,scopes:u(JSON.parse(e.scopes_json)),resolved_scopes:u(JSON.parse(e.resolved_scopes_json)),spend_cap:e.spend_cap,expires_at:e.expires_at,status:e.status,revoked_at:e.revoked_at,approved_at:e.approved_at,payer:e.payer,dependencies:E(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,inference_endpoint:e.inference_endpoint_json?b(JSON.parse(e.inference_endpoint_json)):null,synced_at:e.synced_at}}t(l,"record");function f(e){if(!e||e.status!=="active")return null;try{return j(l(e))}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(f,"active");function d(e){return e.grantee!=="app:terminal"&&O()}t(d,"mirrorOnly");async function P(){return(await F()).filter(e=>!d(e))}t(P,"localGrants");async function F(){return(await(await p()).prepare("SELECT * FROM grants").all()).flatMap(e=>{try{return[l(e)]}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return[];throw r}})}t(F,"allGrants");function M(e){return e.expires_at!==null&&!(Date.parse(e.expires_at)>Date.now())}t(M,"grantExpired");function j(e){if(e.status!=="active"||M(e))return null;try{const r=e.resolved_scopes.map(({scope:n})=>T(n));return{...e,resolved:e.resolved_scopes.flatMap(({mode:n},i)=>n==="always"?[r[i]]:[])}}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(j,"activeRecord");function y(e){return Array.isArray(e)?`[${e.map(y).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(r=>e[r]!==void 0).map(r=>`${JSON.stringify(r)}:${y(e[r])}`).join(",")}}`:JSON.stringify(e)??"null"}t(y,"canonicalJson");function V(e){const r={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{},...e.inference_endpoint?{inference_endpoint:e.inference_endpoint}:{}};return S("sha256").update(y(r)).digest("base64url").slice(0,22)}t(V,"grantVersion");async function B(e){return!!await(await p()).prepare("SELECT 1 FROM grants WHERE id = ?").get(e)}t(B,"hasGrant");async function X(e){return await(await p()).prepare("SELECT rev, status FROM grants WHERE id = ?").get(e)??null}t(X,"grantSyncState");async function Y(e,r){return await(await p()).prepare("SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,r)??null}t(Y,"grantSyncStateFor");async function C(e){const r=await(await p()).prepare("SELECT * FROM grants WHERE id = ?").get(e);if(!r)return null;try{return l(r)}catch(n){if(n instanceof TypeError||n instanceof SyntaxError)return null;throw n}}t(C,"grantRecord");async function q(e){return(e?(await C(e))?.payer:null)??"user"}t(q,"payerOfGrant");async function z(e){return f(await(await p()).prepare("SELECT * FROM grants WHERE id = ?").get(e))}t(z,"activeGrant");async function K(e,r){return f(await(await p()).prepare("SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,r))}t(K,"grantFor");async function Q(e){if(d({grantee:e}))return null;const r=await(await p()).prepare("SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(r.length>1)throw new Error(`multiple active grants for ${e}`);return f(r[0])}t(Q,"activeGrantForApp");async function Z(e){const r=await(await p()).prepare("SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return r&&r.url&&!d(r)?{url:r.url,sub:r.sub,grantee:r.grantee}:null}t(Z,"runNoticeTarget");async function ee(){const e=await(await p()).prepare("SELECT * FROM grants WHERE grantee = 'app:terminal' AND status = 'active'").all();if(e.length>1)throw new Error("multiple active terminal grants");return f(e[0])}t(ee,"terminalGrant");async function re(){return(await(await p()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value??null}t(re,"grantsSource");async function te(e){return(await(await p()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value===e}t(te,"hasSyncedGrants");async function ne(e){return(await e.prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value!=="local"?0:(await e.prepare("DELETE FROM runtime_state WHERE key = 'grants_source'").run(),Number((await e.prepare("DELETE FROM grants").run()).changes))}t(ne,"dropLocalGrantMirror");function ae(e){if(e===void 0)return[];if(!Array.isArray(e))throw new TypeError("invalid grant sync response");return e.map(r=>{const{team:n,members:i}=r??{};if(typeof n!="string"||!/^team:\S+$/.test(n)||!Array.isArray(i)||i.some(o=>typeof o!="string"||!/^user:[^\s*]+$/.test(o)))throw new TypeError("invalid grant sync response");return{team:n,members:i}})}t(ae,"teamsOf");function k(e){return e.status!=="active"||e.spend_cap===null||e.payer==="app"||e.grantee==="app:terminal"?null:{payer:e.sub,grantee:e.grantee,usd_per_month:e.spend_cap}}t(k,"grantLimit");async function se(e,r,n,i=[]){const o=new Set;for(const s of e){if(o.has(s.id)||!s.id||!s.runtime||!s.sub||!s.grantee||!Number.isInteger(s.rev)||!["active","revoked","removed"].includes(s.status))throw new TypeError("invalid grant sync response");o.add(s.id),u(s.scopes),u(s.resolved_scopes)}await w(async s=>{await s.exec("DELETE FROM grants");const _=s.prepare(`INSERT INTO grants
|
|
2
2
|
(id, runtime, sub, grantee, rev, scopes_json, resolved_scopes_json, spend_cap, expires_at,
|
|
3
3
|
status, revoked_at, approved_at, payer, dependencies_json, run_notice_url, inference_endpoint_json, synced_at)
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const a of e)await _.run(a.id,a.runtime,a.sub,a.grantee,a.rev,JSON.stringify(a.scopes),JSON.stringify(a.resolved_scopes),a.spend_cap,a.expires_at,a.status,a.revoked_at,a.approved_at,a.payer,JSON.stringify(E(a.dependencies)),a.run_notice_url??null,a.inference_endpoint?JSON.stringify(a.inference_endpoint):null,a.synced_at);await s.prepare("DELETE FROM limits WHERE source = 'grant_sync'").run();for(const a of e){const c=k(a);c&&await A(a.id,[c],s)}await v(s,"directory",x,"member",i.flatMap(({team:a,members:c})=>c.map(g=>({resourceId:a,subject:R(g)})))),await s.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(n),await s.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(runtime_state.value AS INTEGER) + 1 AS TEXT)").run()})}t(se,"replaceGrants");async function ie(){const e=await(await p()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}t(ie,"grantsGeneration");export{z as activeGrant,Q as activeGrantForApp,j as activeRecord,F as allGrants,E as dependenciesOf,ne as dropLocalGrantMirror,M as grantExpired,K as grantFor,C as grantRecord,X as grantSyncState,Y as grantSyncStateFor,V as grantVersion,ie as grantsGeneration,re as grantsSource,B as hasGrant,te as hasSyncedGrants,b as inferenceEndpointOf,P as localGrants,d as mirrorOnly,q as payerOfGrant,se as replaceGrants,Z as runNoticeTarget,ae as teamsOf,ee as terminalGrant};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const LEDGER_MIGRATED_KEY = "ledger_migrated";
|
|
2
|
+
/** The id of a migrated `usage` row `u`, as SQL: `legacy:<grantee>:<month>:<payer>:<source>:<served_model>`. */
|
|
3
|
+
export declare const LEGACY_ID_SQL = "'legacy:' || u.grantee || ':' || u.month || ':' || u.payer || ':' || u.source || ':' || u.served_model";
|
|
4
|
+
export interface BackfillResult {
|
|
5
|
+
migrated: boolean;
|
|
6
|
+
charges: number;
|
|
7
|
+
limits: number;
|
|
8
|
+
}
|
|
9
|
+
/** Copy `usage` into `charges` as legacy rows, rebuild `charge_totals` from them and fill `limits` from the grant mirror, then
|
|
10
|
+
* verify per `(grantee, month, payer, source, served_model)` that usd and token sums equal the `usage` row's, null kept. A
|
|
11
|
+
* second call sees the marker and does nothing. A failed verification rolls the whole thing back and throws. */
|
|
12
|
+
export declare function backfillLedger(person: string): Promise<BackfillResult>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
var s=Object.defineProperty;var u=(t,e)=>s(t,"name",{value:e,configurable:!0});import{withCanvasWrite as E}from"../canvas/db.js";import{rebuildTotals as o}from"./totals.js";const a="ledger_migrated",r="'legacy:' || u.grantee || ':' || u.month || ':' || u.payer || ':' || u.source || ':' || u.served_model";async function L(t){return await E(async e=>{if(await e.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(a)!==void 0)return{migrated:!1,charges:0,limits:0};const n=await e.prepare(`INSERT INTO charges (id, request, at, month, kind, what, units, usd, payer, funding, sub, grantee, provider)
|
|
2
|
+
SELECT ${r},
|
|
3
|
+
NULL, u.month || '-01T00:00:00.000Z', u.month,
|
|
4
|
+
CASE WHEN u.source = 'model' THEN 'model' ELSE 'tool' END,
|
|
5
|
+
CASE WHEN u.source = 'model' THEN u.served_model ELSE substr(u.source, 10) END,
|
|
6
|
+
CASE WHEN u.source = 'model' THEN jsonb_build_object('input', u.input_tokens, 'output', u.output_tokens, 'cache_read', u.cached_tokens, 'cache_write', 0) END,
|
|
7
|
+
u.usd_equivalent,
|
|
8
|
+
CASE WHEN u.payer = 'app' THEN u.grantee ELSE ?::text END,
|
|
9
|
+
CASE WHEN u.payer = 'app' THEN 'endpoint' WHEN u.source = 'model' THEN 'legacy' ELSE 'bill' END,
|
|
10
|
+
?::text, u.grantee,
|
|
11
|
+
CASE WHEN u.source = 'model' THEN NULL ELSE 'app:' || substr(u.source, 10) END
|
|
12
|
+
FROM usage u ON CONFLICT (id) DO NOTHING`).run(t,t);await o(e);const i=await c(e);return await d(e),await e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value").run(a,new Date().toISOString()),{migrated:!0,charges:n.changes,limits:i}})}u(L,"backfillLedger");async function c(t){return(await t.prepare(`INSERT INTO limits (id, payer, funding, grantee, provider, sub, usd_per_month, source, updated_at)
|
|
13
|
+
SELECT 'grant:' || id, sub, NULL, grantee, NULL, NULL, spend_cap, 'grant_sync', ?
|
|
14
|
+
FROM grants WHERE status = 'active' AND spend_cap IS NOT NULL AND COALESCE(payer, 'user') <> 'app' AND grantee <> 'app:terminal'
|
|
15
|
+
ON CONFLICT (id) DO NOTHING`).run(new Date().toISOString())).changes}u(c,"backfillLimits");async function d(t){const e=await t.prepare(`SELECT u.grantee, u.month, u.payer, u.source, u.served_model FROM usage u
|
|
16
|
+
LEFT JOIN charges c ON c.id = ${r}
|
|
17
|
+
WHERE c.id IS NULL
|
|
18
|
+
OR c.usd IS DISTINCT FROM u.usd_equivalent::numeric(14,6)
|
|
19
|
+
OR (u.source = 'model' AND (COALESCE((c.units ->> 'input')::bigint, -1) <> u.input_tokens
|
|
20
|
+
OR COALESCE((c.units ->> 'output')::bigint, -1) <> u.output_tokens
|
|
21
|
+
OR COALESCE((c.units ->> 'cache_read')::bigint, -1) <> u.cached_tokens))
|
|
22
|
+
LIMIT 5`).all();if(e.length>0)throw new Error(`ledger backfill verification failed; first differing usage rows: ${JSON.stringify(e)}`)}u(d,"verify");export{a as LEDGER_MIGRATED_KEY,r as LEGACY_ID_SQL,L as backfillLedger};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Charge, ChargeInput } from './types.js';
|
|
2
|
+
/** The id of a tool charge: the receipt-level idempotency key, so two providers' receipt ids never collide. */
|
|
3
|
+
export declare const receiptChargeId: (provider: string, receiptId: string) => string;
|
|
4
|
+
export interface WriteResult {
|
|
5
|
+
/** False when `id` already existed: a repeat delivery adds nothing, to the row or the rollup. */
|
|
6
|
+
written: boolean;
|
|
7
|
+
id: string;
|
|
8
|
+
/** The id of the take row written beside it, when `ledger.take_pct` is above 0. */
|
|
9
|
+
takeId: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** The take percentage of `ledger.take_pct`; 0 (the default) writes no take rows. */
|
|
12
|
+
export declare function takePct(): number;
|
|
13
|
+
/** Write one charge row and, in the same transaction, add its usd to its `charge_totals` row. A refused row is stored with
|
|
14
|
+
* usd 0 and never rolled up; a take row (written only while `ledger.take_pct` > 0) is stored and never rolled up. Inside a
|
|
15
|
+
* caller's `withCanvasWrite` it joins that transaction. */
|
|
16
|
+
export declare function writeCharge(input: ChargeInput): Promise<WriteResult>;
|
|
17
|
+
/** A stored row as the `Charge` shape (jsonb arrives as text). */
|
|
18
|
+
export declare function chargeOf(row: Record<string, unknown>): Charge;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var k=Object.defineProperty;var t=(e,r)=>k(e,"name",{value:r,configurable:!0});import{randomUUID as w}from"node:crypto";import{withCanvasWrite as I}from"../canvas/db.js";import{readConfig as y}from"../config.js";import{usageMonth as S}from"./months.js";import{addToTotals as C}from"./totals.js";const x=t((e,r)=>`receipt:${e}:${r}`,"receiptChargeId"),f="id, request, at, month, kind, what, units, usd, payer, funding, sub, grantee, node, run, provider, caused_by, vendor_ref, refused",c="?, ?, ?, ?, ?, ?, ?::jsonb, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb";function T(){const e=y("user").ledger?.take_pct??0;if(typeof e!="number"||!Number.isFinite(e)||e<0||e>100)throw new Error("ledger.take_pct must be a number from 0 to 100");return e}t(T,"takePct");async function R(e){const r=e.at??new Date,n=e.refused??null,s=e.id??w(),d=S(r),o=n?0:e.usd,u=T();return await I(async a=>{const i=t((g,m,N,O,h)=>[g,e.request??null,r.toISOString(),d,m,N,e.units==null?null:JSON.stringify(e.units),O,e.payer,h,e.sub??null,e.grantee,e.node??null,e.run??null,e.provider??null,e.causedBy??null,e.vendorRef??null,n===null?null:JSON.stringify(n)],"row");if((await a.prepare(`INSERT INTO charges (${f}) VALUES (${c}) ON CONFLICT (id) DO NOTHING`).run(...i(s,e.kind,e.what,o,e.funding))).changes===0)return{written:!1,id:s,takeId:null};n===null&&e.kind!=="take"&&await C(a,{month:d,payer:e.payer,funding:e.funding,sub:e.sub??null,grantee:e.grantee,provider:e.provider??null,usd:o});let l=null;return u>0&&n===null&&(e.kind==="model"||e.kind==="tool")&&o!==null&&o>0&&(l=`take:${s}`,await a.prepare(`INSERT INTO charges (${f}) VALUES (${c}) ON CONFLICT (id) DO NOTHING`).run(...i(l,"take",e.what,o*u/100,"take"))),{written:!0,id:s,takeId:l}})}t(R,"writeCharge");function _(e){const r=t(n=>typeof n=="string"?JSON.parse(n):null,"json");return{...e,units:r(e.units),refused:r(e.refused)}}t(_,"chargeOf");export{_ as chargeOf,x as receiptChargeId,T as takePct,R as writeCharge};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RunError } from '../runs/operations.js';
|
|
2
|
+
import type { Funding, Limit } from './types.js';
|
|
3
|
+
/** A model or tool call, checked before it is sent: everything about it is known. A limit matches when its `payer` equals
|
|
4
|
+
* the candidate's and each of its non-null filters equals the candidate's value. */
|
|
5
|
+
export interface CallCandidate {
|
|
6
|
+
scope: 'call';
|
|
7
|
+
payer: string;
|
|
8
|
+
funding: Funding;
|
|
9
|
+
sub: string | null;
|
|
10
|
+
grantee: string;
|
|
11
|
+
provider: string | null;
|
|
12
|
+
}
|
|
13
|
+
/** A node birth, `run.start`, cron fire, job start or dormant revive. `payer` is whoever pays the node's model calls. A limit
|
|
14
|
+
* matches only when its `funding` and `provider` are both null and its other filters match: only a limit that would refuse
|
|
15
|
+
* every charge of that payer (a $0 stop, or app Y's cap on the person) can stop a start. */
|
|
16
|
+
export interface StartCandidate {
|
|
17
|
+
scope: 'start';
|
|
18
|
+
payer: string;
|
|
19
|
+
sub: string | null;
|
|
20
|
+
grantee: string;
|
|
21
|
+
}
|
|
22
|
+
export type Candidate = CallCandidate | StartCandidate;
|
|
23
|
+
/** The limit that refused and what had been spent against it. */
|
|
24
|
+
export interface Refusal {
|
|
25
|
+
limit: Limit;
|
|
26
|
+
usd: number;
|
|
27
|
+
month: string;
|
|
28
|
+
reset_at: string;
|
|
29
|
+
}
|
|
30
|
+
/** The first limit (by id) that refuses `candidate` this month, or null. One statement. */
|
|
31
|
+
export declare function findRefusal(candidate: Candidate, at?: Date): Promise<Refusal | null>;
|
|
32
|
+
/** The `user_action` a person can take for a limit of this source; absent for an operator limit. */
|
|
33
|
+
export declare function userActionOf(source: Limit['source']): 'raise_cap' | 'clear_limit' | undefined;
|
|
34
|
+
/** `402 cap_exhausted` for a refusal. Details: `limit` (id, payer, filters, usd_per_month, source), `usd`, `reset_at`, and
|
|
35
|
+
* `user_action` when the person can act; plus the deprecated `spend_cap` (= `limit.usd_per_month`) and `usd_equivalent` (= `usd`)
|
|
36
|
+
* that the refusal carried before the ledger, kept so a caller that reads them still gets the numbers. */
|
|
37
|
+
export declare function refusalError(refusal: Refusal): RunError;
|
|
38
|
+
/** Throw `refusalError` when a limit refuses `candidate`. */
|
|
39
|
+
export declare function check(candidate: Candidate, at?: Date): Promise<void>;
|
|
40
|
+
/** The mark stored on the refused row a site writes before it moves to the next candidate. */
|
|
41
|
+
export declare const refusedMark: (refusal: Refusal) => {
|
|
42
|
+
limit: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var a=Object.defineProperty;var n=(e,r)=>a(e,"name",{value:r,configurable:!0});import{openDb as d}from"../canvas/db.js";import{RunError as c}from"../runs/operations.js";import{resetAt as L,usageMonth as m}from"./months.js";const g=`(SELECT COALESCE(SUM(t.usd), 0) FROM charge_totals t WHERE t.month = ? AND t.payer = l.payer
|
|
2
|
+
AND (l.funding IS NULL OR t.funding = l.funding) AND (l.grantee IS NULL OR t.grantee = l.grantee)
|
|
3
|
+
AND (l.provider IS NULL OR t.provider = l.provider) AND (l.sub IS NULL OR t.sub = l.sub))`;async function N(e,r=new Date){const t=m(r),s=e.sub??"",o=e.scope==="call"?`l.payer = ? AND (l.funding IS NULL OR l.funding = ?) AND (l.grantee IS NULL OR l.grantee = ?)
|
|
4
|
+
AND (l.provider IS NULL OR l.provider = ?) AND (l.sub IS NULL OR l.sub = ?)`:"l.payer = ? AND l.funding IS NULL AND l.provider IS NULL AND (l.grantee IS NULL OR l.grantee = ?) AND (l.sub IS NULL OR l.sub = ?)",u=e.scope==="call"?[e.payer,e.funding,e.grantee,e.provider??"",s]:[e.payer,e.grantee,s],i=await(await d()).prepareUncached(`SELECT * FROM (
|
|
5
|
+
SELECT l.id, l.payer, l.funding, l.grantee, l.provider, l.sub, l.usd_per_month, l.source, l.updated_at, ${g} AS spent
|
|
6
|
+
FROM limits l WHERE ${o}) q
|
|
7
|
+
WHERE q.spent >= q.usd_per_month ORDER BY q.id LIMIT 1`).get(t,...u);if(i===void 0)return null;const{spent:l,...p}=i;return{limit:p,usd:l,month:t,reset_at:L(t)}}n(N,"findRefusal");function f(e){return e==="grant_sync"?"raise_cap":e==="person"?"clear_limit":void 0}n(f,"userActionOf");function _(e){const{limit:r}=e,t=f(r.source);return new c(402,"cap_exhausted",`${r.payer} reached its monthly limit (${r.id})`,void 0,{limit:{id:r.id,payer:r.payer,filters:{funding:r.funding,grantee:r.grantee,provider:r.provider,sub:r.sub},usd_per_month:r.usd_per_month,source:r.source},usd:e.usd,reset_at:e.reset_at,spend_cap:r.usd_per_month,usd_equivalent:e.usd,...t===void 0?{}:{user_action:t}})}n(_,"refusalError");async function E(e,r){const t=await N(e,r);if(t!==null)throw _(t)}n(E,"check");const D=n(e=>({limit:e.limit.id,reason:`${e.limit.payer} reached ${e.limit.usd_per_month} USD this month (spent ${e.usd})`}),"refusedMark");export{E as check,N as findRefusal,_ as refusalError,D as refusedMark,f as userActionOf};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './months.js';
|
|
3
|
+
export { payerOf, type Payer, type PayerCall } from './payer.js';
|
|
4
|
+
export { writeCharge, receiptChargeId, takePct, chargeOf, type WriteResult } from './charges.js';
|
|
5
|
+
export { check, findRefusal, refusalError, refusedMark, userActionOf, type Candidate, type CallCandidate, type StartCandidate, type Refusal } from './check.js';
|
|
6
|
+
export { upsertLimit, deleteLimit, replaceGrantLimits, listLimits, grantLimitId } from './limits.js';
|
|
7
|
+
export { usageFor, listCharges, monthTotals, assertReference, type ChargePage, type ListFilter, type MonthTotals } from './read.js';
|
|
8
|
+
export { backfillLedger, LEDGER_MIGRATED_KEY, type BackfillResult } from './backfill.js';
|
|
9
|
+
export { rebuildTotals, rebuildTotalsFor } from './totals.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export*from"./types.js";export*from"./months.js";import{payerOf as i}from"./payer.js";import{writeCharge as f,receiptChargeId as s,takePct as m,chargeOf as l}from"./charges.js";import{check as c,findRefusal as x,refusalError as d,refusedMark as u,userActionOf as g}from"./check.js";import{upsertLimit as h,deleteLimit as n,replaceGrantLimits as E,listLimits as k,grantLimitId as R}from"./limits.js";import{usageFor as b,listCharges as C,monthTotals as G,assertReference as I}from"./read.js";import{backfillLedger as A,LEDGER_MIGRATED_KEY as D}from"./backfill.js";import{rebuildTotals as M,rebuildTotalsFor as _}from"./totals.js";export{D as LEDGER_MIGRATED_KEY,I as assertReference,A as backfillLedger,l as chargeOf,c as check,n as deleteLimit,x as findRefusal,R as grantLimitId,C as listCharges,k as listLimits,G as monthTotals,i as payerOf,M as rebuildTotals,_ as rebuildTotalsFor,s as receiptChargeId,d as refusalError,u as refusedMark,E as replaceGrantLimits,m as takePct,h as upsertLimit,b as usageFor,g as userActionOf,f as writeCharge};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CanvasDb } from '../canvas/db.js';
|
|
2
|
+
import type { Limit, LimitInput, LimitSource } from './types.js';
|
|
3
|
+
export declare const grantLimitId: (grantId: string) => string;
|
|
4
|
+
/** Insert or replace one limit row by id. */
|
|
5
|
+
export declare function upsertLimit(input: LimitInput): Promise<Limit>;
|
|
6
|
+
/** Delete one limit row; true when it existed. */
|
|
7
|
+
export declare function deleteLimit(id: string): Promise<boolean>;
|
|
8
|
+
/** A grant's limit rows in one transaction: delete every `grant_sync` row of this grant, then write `rows`. The first row's id
|
|
9
|
+
* is `grant:<grant id>`, later ones `grant:<grant id>#<n>`. Only `grant_sync` rows are touched. */
|
|
10
|
+
export declare function replaceGrantLimits(grantId: string, rows: ReadonlyArray<Omit<LimitInput, 'id' | 'source'>>, db?: CanvasDb): Promise<void>;
|
|
11
|
+
/** Limit rows, optionally of one source or one payer, in id order. */
|
|
12
|
+
export declare function listLimits(filter?: {
|
|
13
|
+
source?: LimitSource;
|
|
14
|
+
payer?: string;
|
|
15
|
+
}): Promise<Limit[]>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var l=Object.defineProperty;var a=(n,e)=>l(n,"name",{value:e,configurable:!0});import{openDb as m,withCanvasWrite as i}from"../canvas/db.js";const u=a(n=>`grant:${n}`,"grantLimitId"),s="id, payer, funding, grantee, provider, sub, usd_per_month, source, updated_at";async function E(n){if(!Number.isFinite(n.usd_per_month)||n.usd_per_month<0)throw new RangeError("usd_per_month must be a number of at least 0");const e={funding:null,grantee:null,provider:null,sub:null,...n,updated_at:new Date().toISOString()};return await i(async r=>{await r.prepare(`INSERT INTO limits (${s}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2
|
+
ON CONFLICT (id) DO UPDATE SET payer = excluded.payer, funding = excluded.funding, grantee = excluded.grantee,
|
|
3
|
+
provider = excluded.provider, sub = excluded.sub, usd_per_month = excluded.usd_per_month, source = excluded.source, updated_at = excluded.updated_at`).run(e.id,e.payer,e.funding,e.grantee,e.provider,e.sub,e.usd_per_month,e.source,e.updated_at)}),e}a(E,"upsertLimit");async function w(n){return await i(async e=>(await e.prepare("DELETE FROM limits WHERE id = ?").run(n)).changes>0)}a(w,"deleteLimit");async function f(n,e,r){const d=a(async o=>{await o.prepare("DELETE FROM limits WHERE source = 'grant_sync' AND (id = ? OR starts_with(id, ?))").run(u(n),`${u(n)}#`);const c=new Date().toISOString();for(const[p,t]of e.entries()){if(!Number.isFinite(t.usd_per_month)||t.usd_per_month<0)throw new RangeError("usd_per_month must be a number of at least 0");await o.prepare(`INSERT INTO limits (${s}) VALUES (?, ?, ?, ?, ?, ?, ?, 'grant_sync', ?)`).run(p===0?u(n):`${u(n)}#${p}`,t.payer,t.funding??null,t.grantee??null,t.provider??null,t.sub??null,t.usd_per_month,c)}},"body");r!==void 0&&r.inTransaction?await d(r):await i(d)}a(f,"replaceGrantLimits");async function g(n={}){const e=[],r=[];return n.source!==void 0&&(e.push("source = ?"),r.push(n.source)),n.payer!==void 0&&(e.push("payer = ?"),r.push(n.payer)),await(await m()).prepareUncached(`SELECT ${s} FROM limits${e.length?` WHERE ${e.join(" AND ")}`:""} ORDER BY id`).all(...r)}a(g,"listLimits");export{w as deleteLimit,u as grantLimitId,g as listLimits,f as replaceGrantLimits,E as upsertLimit};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The UTC calendar month `YYYY-MM` of `at`. */
|
|
2
|
+
export declare function usageMonth(at?: Date): string;
|
|
3
|
+
/** 00:00Z on the first day of the UTC month after `month`. */
|
|
4
|
+
export declare function resetAt(month: string): string;
|
|
5
|
+
/** A `month` query value: `YYYY-MM` with a real month, years 1000-9999; absent means the current month. */
|
|
6
|
+
export declare function parseUsageMonth(value: string | null): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=Object.defineProperty;var n=(t,r)=>o(t,"name",{value:r,configurable:!0});import{RunError as i}from"../runs/operations.js";function u(t=new Date){return t.toISOString().slice(0,7)}n(u,"usageMonth");function a(t){const[r,e]=t.split("-").map(Number);return new Date(Date.UTC(r,e,1)).toISOString()}n(a,"resetAt");function p(t){if(t===null)return u();if(!/^[1-9]\d{3}-(0[1-9]|1[0-2])$/.test(t))throw new i(400,"invalid_request","month must be YYYY-MM","month");return t}n(p,"parseUsageMonth");export{p as parseUsageMonth,a as resetAt,u as usageMonth};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Funding } from './types.js';
|
|
2
|
+
export interface Payer {
|
|
3
|
+
payer: string;
|
|
4
|
+
funding: Funding;
|
|
5
|
+
}
|
|
6
|
+
export type PayerCall =
|
|
7
|
+
/** A model call on an app-paid grant: the app's inference endpoint pays. `grantee` is the app principal (`app:<id>`). */
|
|
8
|
+
{
|
|
9
|
+
call: 'model';
|
|
10
|
+
route: 'endpoint';
|
|
11
|
+
grantee: string;
|
|
12
|
+
}
|
|
13
|
+
/** A model call on a managed subscription account from the pool, or on an API key (incl. a managed provider reached
|
|
14
|
+
* through a key, which is `key`, never `subscription`). `sub` is the person the work is for. */
|
|
15
|
+
| {
|
|
16
|
+
call: 'model';
|
|
17
|
+
route: 'subscription' | 'key';
|
|
18
|
+
sub: string;
|
|
19
|
+
}
|
|
20
|
+
/** A tool call over HTTP: the outbound token's `payer` claim (`app:<id>` when the app pays, else the person). */
|
|
21
|
+
| {
|
|
22
|
+
call: 'tool';
|
|
23
|
+
tokenPayer: string;
|
|
24
|
+
sub: string;
|
|
25
|
+
}
|
|
26
|
+
/** The payer of a node's model calls, for the start check: `app:<grantee>` when the grant is app-paid, else the person. */
|
|
27
|
+
| {
|
|
28
|
+
call: 'start';
|
|
29
|
+
grantPayer: 'user' | 'app' | null | undefined;
|
|
30
|
+
sub: string;
|
|
31
|
+
grantee: string;
|
|
32
|
+
};
|
|
33
|
+
/** Who pays, and with what funding. A device call is not priced and never asks. */
|
|
34
|
+
export declare function payerOf(call: PayerCall): Payer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var t=Object.defineProperty;var n=(e,r)=>t(e,"name",{value:r,configurable:!0});function i(e){switch(e.call){case"model":return e.route==="endpoint"?{payer:e.grantee,funding:"endpoint"}:{payer:e.sub,funding:e.route};case"tool":return e.tokenPayer.startsWith("app:")?{payer:e.tokenPayer,funding:"endpoint"}:{payer:e.sub,funding:"bill"};case"start":return e.grantPayer==="app"?{payer:e.grantee,funding:"endpoint"}:{payer:e.sub,funding:"bill"}}}n(i,"payerOf");export{i as payerOf};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SpendUsageDTO } from '../../api/dto/usage.js';
|
|
2
|
+
import type { Charge, Limit } from './types.js';
|
|
3
|
+
/** `usage.get` for one grantee and month, computed from `charges`. `by_model` and `app_paid.by_model` group the `model` rows
|
|
4
|
+
* of a `user:` / `app:` payer by `what`, with token sums from `units` and `usd_equivalent` null when every row's `usd` is null.
|
|
5
|
+
* `usd_equivalent` is what the grant's limit reads: every `user:` payer charge for this grantee, model and tool, refused and
|
|
6
|
+
* take rows excluded. `spend_cap` is the grant limit's `usd_per_month` for this grantee, null when none exists. */
|
|
7
|
+
export declare function usageFor(grantee: string, month?: string): Promise<SpendUsageDTO>;
|
|
8
|
+
export interface ListFilter {
|
|
9
|
+
/** Only rows of this month. */
|
|
10
|
+
month?: string;
|
|
11
|
+
/** Only rows of this grantee (an app caller sees its own). */
|
|
12
|
+
grantee?: string;
|
|
13
|
+
/** A reference: `message:<id>`, `request:<id>`, `cron_run:<id>`, `run:<id>`, or a charge id. */
|
|
14
|
+
under?: string;
|
|
15
|
+
/** Page size, 1 to 10,000; default 1,000. */
|
|
16
|
+
limit?: number;
|
|
17
|
+
/** The `next` of the previous page: continue after that page's last row. */
|
|
18
|
+
after?: string;
|
|
19
|
+
}
|
|
20
|
+
/** One page of rows in `(at, id)` order. `next` continues after the last row; null when this page is the last. */
|
|
21
|
+
export interface ChargePage {
|
|
22
|
+
charges: Charge[];
|
|
23
|
+
next: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** A page of rows in `(at, id)` order, newest last: the rows under a reference when `under` is set (every month unless `month`
|
|
26
|
+
* is given), otherwise the month's rows (default the current month). */
|
|
27
|
+
export declare function listCharges(filter?: ListFilter): Promise<ChargePage>;
|
|
28
|
+
export interface MonthTotals {
|
|
29
|
+
month: string;
|
|
30
|
+
by_payer: Array<{
|
|
31
|
+
payer: string;
|
|
32
|
+
usd: number;
|
|
33
|
+
}>;
|
|
34
|
+
by_funding: Array<{
|
|
35
|
+
payer: string;
|
|
36
|
+
funding: string;
|
|
37
|
+
usd: number;
|
|
38
|
+
}>;
|
|
39
|
+
limits: Limit[];
|
|
40
|
+
}
|
|
41
|
+
/** One month's dollars per payer and per payer and funding, from the rollup, with the limit rows. Optionally for one payer. */
|
|
42
|
+
export declare function monthTotals(month?: string, payer?: string): Promise<MonthTotals>;
|
|
43
|
+
/** A reference's shape, or a 400. */
|
|
44
|
+
export declare function assertReference(value: string): string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var _=Object.defineProperty;var a=(e,n)=>_(e,"name",{value:n,configurable:!0});import{openDb as f}from"../canvas/db.js";import{RunError as h}from"../runs/operations.js";import{chargeOf as g}from"./charges.js";import{listLimits as E}from"./limits.js";import{resetAt as y,usageMonth as l}from"./months.js";const N="id, request, at, month, kind, what, units, usd, payer, funding, sub, grantee, node, run, provider, caused_by, vendor_ref, refused";async function v(e,n=l()){const t=await f(),s=await t.prepare(`SELECT payer, what, kind, units, usd FROM charges
|
|
2
|
+
WHERE grantee = ? AND month = ? AND refused IS NULL AND kind <> 'take'`).all(e,n),o=a(u=>{const p=new Map;for(const r of s){if(r.kind!=="model"||!r.payer.startsWith(u))continue;const m=r.units===null?{}:JSON.parse(r.units),c=p.get(r.what)??{served_model:r.what,input_tokens:0,output_tokens:0,usd_equivalent:null};c.input_tokens+=m.input??0,c.output_tokens+=m.output??0,r.usd!==null&&(c.usd_equivalent=(c.usd_equivalent??0)+Number(r.usd)),p.set(r.what,c)}return[...p.values()].sort((r,m)=>r.served_model<m.served_model?-1:1)},"byModel"),i=a(u=>s.filter(p=>p.payer.startsWith(u)).reduce((p,r)=>p+Number(r.usd??0),0),"sum"),d=await t.prepare("SELECT usd_per_month FROM limits WHERE source = 'grant_sync' AND grantee = ? AND payer LIKE 'user:%' AND funding IS NULL AND provider IS NULL ORDER BY id LIMIT 1").get(e);return{grantee:e,month:n,usd_equivalent:i("user:"),by_model:o("user:"),app_paid:{usd_equivalent:i("app:"),by_model:o("app:")},spend_cap:d===void 0?null:Number(d.usd_per_month),reset_at:y(n)}}a(v,"usageFor");const R=/^(message|request|cron_run|run):(.+)$/;function b(e){const n=R.exec(e);if(n===null)return{sql:"id = ?",params:[e]};if(n[1]==="message"||n[1]==="cron_run")return{sql:"caused_by = ?",params:[e]};if(n[1]==="request")return{sql:"(request = ? OR caused_by = ?)",params:[n[2],e]};const t=n[2];return{sql:`(run = ? OR run IN (SELECT n.run_id FROM nodes n WHERE n.run_id IS NOT NULL AND n.node_id = n.run_id AND n.caused_by IN (
|
|
3
|
+
SELECT 'request:' || c.request FROM charges c WHERE c.run = ? AND c.request IS NOT NULL)))`,params:[t,t]}}a(b,"referenceCondition");const L=a(e=>Buffer.from(JSON.stringify([e.at,e.id]),"utf8").toString("base64url"),"encodeCursor");function O(e){try{const n=JSON.parse(Buffer.from(e,"base64url").toString("utf8"));if(Array.isArray(n)&&n.length===2&&typeof n[0]=="string"&&typeof n[1]=="string")return[n[0],n[1]]}catch{}throw new h(400,"invalid_request","cursor must be a `next` value from a previous page","after")}a(O,"decodeCursor");async function C(e={}){const n=Math.min(Math.max(Math.trunc(e.limit??1e3),1),1e4),t=[],s=[];if(e.under!==void 0){const d=b(e.under);t.push(d.sql),s.push(...d.params),e.month!==void 0&&(t.push("month = ?"),s.push(e.month))}else t.push("month = ?"),s.push(e.month??l());e.grantee!==void 0&&(t.push("grantee = ?"),s.push(e.grantee)),e.after!==void 0&&(t.push("(at, id) > (?, ?)"),s.push(...O(e.after)));const o=(await(await f()).prepare(`SELECT ${N} FROM charges WHERE ${t.join(" AND ")} ORDER BY at, id LIMIT ${n+1}`).all(...s)).map(g),i=o.slice(0,n);return{charges:i,next:o.length>n?L(i[n-1]):null}}a(C,"listCharges");async function I(e=l(),n){const t=await f(),s=n===void 0?"":" AND payer = ?",o=n===void 0?[e]:[e,n],i=await t.prepare(`SELECT payer, SUM(usd) AS usd FROM charge_totals WHERE month = ?${s} GROUP BY payer ORDER BY payer`).all(...o),d=await t.prepare(`SELECT payer, funding, SUM(usd) AS usd FROM charge_totals WHERE month = ?${s} GROUP BY payer, funding ORDER BY payer, funding`).all(...o);return{month:e,by_payer:i.map(u=>({payer:u.payer,usd:Number(u.usd)})),by_funding:d.map(u=>({payer:u.payer,funding:u.funding,usd:Number(u.usd)})),limits:await E(n===void 0?{}:{payer:n})}}a(I,"monthTotals");function T(e){if(e.length===0||e.length>512)throw new h(400,"invalid_request","under must be a reference or a charge id","under");return e}a(T,"assertReference");export{T as assertReference,C as listCharges,I as monthTotals,v as usageFor};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CanvasDb } from '../canvas/db.js';
|
|
2
|
+
/** `charge_totals` stores an absent `sub` or `provider` as ''. */
|
|
3
|
+
export declare const absent: (value: string | null | undefined) => string;
|
|
4
|
+
/** Add `usd` (null adds 0) to the rollup row of one charge, in the caller's transaction. Concurrent writers to one key
|
|
5
|
+
* serialize on the conflicting row, so the total is exact. */
|
|
6
|
+
export declare function addToTotals(db: CanvasDb, charge: {
|
|
7
|
+
month: string;
|
|
8
|
+
payer: string;
|
|
9
|
+
funding: string;
|
|
10
|
+
sub: string | null;
|
|
11
|
+
grantee: string;
|
|
12
|
+
provider: string | null;
|
|
13
|
+
usd: number | null;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/** Rebuild the whole rollup from `charges`. */
|
|
16
|
+
export declare function rebuildTotals(db: CanvasDb): Promise<void>;
|
|
17
|
+
/** Delete the rollup rows that name `a` or `b` as payer, sub or grantee and rebuild them from `charges`, so two rows that
|
|
18
|
+
* now share a key are summed once. For person substitution, in the same transaction as the rename. */
|
|
19
|
+
export declare function rebuildTotalsFor(db: CanvasDb, a: string, b: string): Promise<void>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var s=Object.defineProperty;var t=(r,e)=>s(r,"name",{value:e,configurable:!0});const a=t(r=>r??"","absent");async function p(r,e){await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2
|
+
ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE SET usd = charge_totals.usd + excluded.usd`).run(e.month,e.payer,e.funding,a(e.sub),e.grantee,a(e.provider),e.usd??0)}t(p,"addToTotals");const u=`SELECT month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, ''), COALESCE(SUM(usd), 0)
|
|
3
|
+
FROM charges WHERE refused IS NULL AND kind <> 'take'`,o="GROUP BY month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, '')";async function d(r){await r.prepare("DELETE FROM charge_totals").run(),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd) ${u} ${o}`).run()}t(d,"rebuildTotals");async function i(r,e,n){await r.prepare("DELETE FROM charge_totals WHERE payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)").run(e,n,e,n,e,n),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd) ${u}
|
|
4
|
+
AND (payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)) ${o}`).run(e,n,e,n,e,n)}t(i,"rebuildTotalsFor");export{a as absent,p as addToTotals,d as rebuildTotals,i as rebuildTotalsFor};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type ChargeKind = 'model' | 'tool' | 'sandbox' | 'job' | 'storage' | 'take';
|
|
2
|
+
export type Funding = 'bill' | 'subscription' | 'key' | 'endpoint' | 'take' | 'legacy';
|
|
3
|
+
export type LimitSource = 'grant_sync' | 'person' | 'operator';
|
|
4
|
+
/** Why the check refused a call: the limit's id and a sentence. Stored on the refused row. */
|
|
5
|
+
export interface RefusedMark {
|
|
6
|
+
limit: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
/** One `charges` row as stored. */
|
|
10
|
+
export interface Charge {
|
|
11
|
+
id: string;
|
|
12
|
+
request: string | null;
|
|
13
|
+
at: string;
|
|
14
|
+
month: string;
|
|
15
|
+
kind: ChargeKind;
|
|
16
|
+
what: string;
|
|
17
|
+
/** `{input, output, cache_read, cache_write}` tokens, or `{units, unit}` from a receipt; null on a legacy tool row. */
|
|
18
|
+
units: Record<string, unknown> | null;
|
|
19
|
+
usd: number | null;
|
|
20
|
+
payer: string;
|
|
21
|
+
funding: Funding;
|
|
22
|
+
sub: string | null;
|
|
23
|
+
grantee: string;
|
|
24
|
+
node: string | null;
|
|
25
|
+
run: string | null;
|
|
26
|
+
provider: string | null;
|
|
27
|
+
caused_by: string | null;
|
|
28
|
+
vendor_ref: string | null;
|
|
29
|
+
refused: RefusedMark | null;
|
|
30
|
+
}
|
|
31
|
+
/** What a site hands `writeCharge`. */
|
|
32
|
+
export interface ChargeInput {
|
|
33
|
+
/** `receipt:<provider>:<receipt_id>` for a tool charge; a fresh id when absent. A repeat of an existing id adds nothing. */
|
|
34
|
+
id?: string;
|
|
35
|
+
request?: string | null;
|
|
36
|
+
at?: Date;
|
|
37
|
+
kind: ChargeKind;
|
|
38
|
+
what: string;
|
|
39
|
+
units?: Record<string, unknown> | null;
|
|
40
|
+
/** The list-price amount; null when the model has no list price. Ignored (0 is stored) when `refused` is set. */
|
|
41
|
+
usd: number | null;
|
|
42
|
+
payer: string;
|
|
43
|
+
funding: Funding;
|
|
44
|
+
sub?: string | null;
|
|
45
|
+
grantee: string;
|
|
46
|
+
node?: string | null;
|
|
47
|
+
run?: string | null;
|
|
48
|
+
provider?: string | null;
|
|
49
|
+
causedBy?: string | null;
|
|
50
|
+
vendorRef?: string | null;
|
|
51
|
+
/** Set when the check refused this call: the row is stored with usd 0 and is not added to the rollup. */
|
|
52
|
+
refused?: RefusedMark | null;
|
|
53
|
+
}
|
|
54
|
+
/** One `limits` row. A null filter means any. */
|
|
55
|
+
export interface Limit {
|
|
56
|
+
id: string;
|
|
57
|
+
payer: string;
|
|
58
|
+
funding: Funding | null;
|
|
59
|
+
grantee: string | null;
|
|
60
|
+
provider: string | null;
|
|
61
|
+
sub: string | null;
|
|
62
|
+
usd_per_month: number;
|
|
63
|
+
source: LimitSource;
|
|
64
|
+
updated_at: string;
|
|
65
|
+
}
|
|
66
|
+
export type LimitInput = Omit<Limit, 'updated_at' | 'funding' | 'grantee' | 'provider' | 'sub'> & Partial<Pick<Limit, 'funding' | 'grantee' | 'provider' | 'sub'>>;
|
|
File without changes
|
package/dist/core/llm/run.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type ThinkingLevel } from '@earendil-works/pi-ai/compat';
|
|
2
|
-
import { type SpendTarget } from '../usage/ledger.js';
|
|
3
2
|
import type { GrantRecord, InferenceEndpoint } from '../grants/mirror.js';
|
|
4
3
|
type WireContent = string | null | Array<{
|
|
5
4
|
type: 'text';
|
|
@@ -63,11 +62,24 @@ export interface LlmCaller {
|
|
|
63
62
|
inference_endpoint: InferenceEndpoint | null;
|
|
64
63
|
}
|
|
65
64
|
export declare function llmCallerOf(grant: Pick<GrantRecord, 'payer' | 'sub' | 'grantee' | 'inference_endpoint'> | null | undefined): LlmCaller | undefined;
|
|
66
|
-
/**
|
|
67
|
-
* `
|
|
68
|
-
* is
|
|
65
|
+
/** Who a call is attributed to in the ledger. `grantee` is the app the charge belongs to (`app:terminal` for the person's own agent);
|
|
66
|
+
* `sub` the person the work is for (the runtime's person when absent); `node` and `run` place the row under its run;
|
|
67
|
+
* `request` is the `/v1/llm` `Request-Id` stored on the row (the completion id when absent).
|
|
68
|
+
* `spend_cap` is ignored: limits are `limits` rows the ledger check reads; a caller still passing it is L5's to drop. */
|
|
69
|
+
export interface LlmSpend {
|
|
70
|
+
grantee: string;
|
|
71
|
+
sub?: string | null;
|
|
72
|
+
node?: string | null;
|
|
73
|
+
run?: string | null;
|
|
74
|
+
request?: string;
|
|
75
|
+
spend_cap?: number | null;
|
|
76
|
+
}
|
|
77
|
+
/** `spend`: who the call is attributed to. With it, the ledger check runs for each candidate route before its vendor request
|
|
78
|
+
* (a refused route writes a refused row and the next candidate is tried; all refused is `cap_exhausted` with the first refusal)
|
|
79
|
+
* and a completed call writes one `charges` row. Omitted, the call is neither checked nor recorded.
|
|
80
|
+
* `causedBy`: the request's `Crtr-Caused-By` value (`message:<id>`, `request:<id>`, `cron_run:<id>`), when it carried one; stored on the row. */
|
|
69
81
|
export declare function runLlm(input: LlmRequest, signal?: AbortSignal, onChunk?: (chunk: Record<string, unknown>) => void, target?: {
|
|
70
82
|
cwd: string;
|
|
71
83
|
profileId: string | null;
|
|
72
|
-
}, spend?:
|
|
84
|
+
}, spend?: LlmSpend, caller?: LlmCaller, causedBy?: string): Promise<LlmResult>;
|
|
73
85
|
export {};
|
package/dist/core/llm/run.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Ae=Object.defineProperty;var c=(e,t)=>Ae(e,"name",{value:t,configurable:!0});import{randomUUID as Re}from"node:crypto";import{streamSimple as $e}from"@earendil-works/pi-ai/compat";import{APP_FAMILY as Ie,expandModelCandidates as ue,parseModelSpec as fe,probeRouteAvailability as Ce}from"../model-routes.js";import{readMergedLaunchConfig as me}from"../config.js";import{openModelRegistry as Oe}from"../runtime/model-registry.js";import{MAX_RATE_LIMIT_COOLDOWN_MS as pe,coolingUntilFor as he,isManagedProvider as K,markSubscriptionRateLimited as Se,markSubscriptionSuccess as Le,readSubscriptionPool as Ee,retryAfterMsFromHeaders as z,retryAfterMsFromError as Fe,refreshSubscriptionCredential as Ne,subscriptionNeedsSuccessWrite as De}from"../subscription-state.js";import{classifyRateLimit as Pe,describeRateLimitScope as _e,rateLimitHeaders as Te}from"../rate-limit-scope.js";import{RunError as s}from"../runs/operations.js";import{recognizeProviderError as re}from"../provider-error-recognition.js";import{isSafetyRefusal as Je}from"./refusal.js";import{findRefusal as We,payerOf as ge,refusalError as Ue,refusedMark as Be,writeCharge as ye}from"../ledger/index.js";import{personIdAsync as He}from"../canvas/person.js";import{getNode as we}from"../canvas/canvas.js";import{outboundToken as Ke}from"../providers/mint.js";import{discardFile as ze}from"../providers/token-cache.js";import{ProviderError as Xe}from"../providers/errors.js";import{appCandidates as Ge}from"./app-rungs.js";function Ye(e){return typeof e=="string"||e===null||Array.isArray(e)&&e.every(t=>t&&typeof t=="object"&&(t.type==="text"&&typeof t.text=="string"||t.type==="image_url"&&typeof t.image_url?.url=="string"&&/^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(t.image_url.url)))}c(Ye,"validContent");function be(e){return Array.isArray(e)?e.map(t=>{if(t.type==="text")return t;const[,n,i]=/^data:(image\/(?:png|jpeg|gif|webp));base64,(.*)$/.exec(t.image_url.url);return{type:"image",mimeType:n,data:i}}):e??""}c(be,"imageContent");function Ze(e,t){const n=(e instanceof Error?e.message:String(e)).split("; stack=",1)[0],i=re({message:n}).kind;return i!=="connection"&&i!=="connection-only"?e:new s(503,"upstream_model_unavailable",n.slice(0,500),void 0,{served_model:t})}c(Ze,"refreshFailure");function qt(e){if(!e||typeof e!="object"||Array.isArray(e))throw new s(400,"invalid_request","body must be an object");const t=e;if(!Array.isArray(t.messages)||!t.messages.every(n=>n&&typeof n=="object"&&["system","user","assistant","tool"].includes(n.role)&&Ye(n.content)))throw new s(400,"invalid_request","messages must be chat messages","messages");if(t.messages.some(n=>["system","assistant"].includes(n.role)&&typeof n.content!="string"&&n.content!==null))throw new s(400,"invalid_request","system and assistant content must be text","messages");if(t.model!==void 0&&typeof t.model!="string")throw new s(400,"invalid_request","model must be a string","model");if(t.model_policy!==void 0&&!["exact","substitute"].includes(t.model_policy))throw new s(400,"invalid_request","invalid model_policy","model_policy");if(t.payer!==void 0&&!["user","app"].includes(t.payer))throw new s(400,"invalid_request","invalid payer","payer");if(t.reasoning_effort!==void 0&&!["minimal","low","medium","high","xhigh","max"].includes(t.reasoning_effort))throw new s(400,"invalid_request","invalid reasoning_effort","reasoning_effort");if(t.stream!==void 0&&typeof t.stream!="boolean")throw new s(400,"invalid_request","stream must be boolean","stream");if(t.tools!==void 0&&(!Array.isArray(t.tools)||!t.tools.every(n=>n?.type==="function"&&typeof n.function?.name=="string"&&n.function.name.length>0&&(n.function.description===void 0||typeof n.function.description=="string")&&(n.function.parameters===void 0||n.function.parameters!==null&&typeof n.function.parameters=="object"&&!Array.isArray(n.function.parameters)&&n.function.parameters.type==="object"))))throw new s(400,"invalid_request","invalid tools","tools");if(t.tool_choice!==void 0&&t.tool_choice!=="auto"&&t.tool_choice!=="none"&&t.tool_choice!=="required"&&(typeof t.tool_choice!="object"||t.tool_choice===null||t.tool_choice.type!=="function"||typeof t.tool_choice.function?.name!="string"))throw new s(400,"invalid_request","invalid tool_choice","tool_choice");if(t.messages.some(n=>n.role==="assistant"&&n.tool_calls!==void 0&&(!Array.isArray(n.tool_calls)||!n.tool_calls.every(i=>i&&typeof i.id=="string"&&typeof i.function?.name=="string"&&typeof i.function.arguments=="string"&&Qe(i.function.arguments)))))throw new s(400,"invalid_request","invalid assistant tool_calls","messages");if(t.stop!==void 0&&!(typeof t.stop=="string"||Array.isArray(t.stop)&&t.stop.every(n=>typeof n=="string")))throw new s(400,"invalid_request","invalid stop","stop");if(t.seed!==void 0&&!Number.isSafeInteger(t.seed))throw new s(400,"invalid_request","invalid seed","seed");for(const[n,i]of[["temperature",t.temperature],["top_p",t.top_p],["max_tokens",t.max_tokens]])if(i!==void 0&&(typeof i!="number"||!Number.isFinite(i)||n==="temperature"&&(i<0||i>2)||n==="top_p"&&(i<0||i>1)||n==="max_tokens"&&(!Number.isSafeInteger(i)||i<1)))throw new s(400,"invalid_request",`invalid ${n}`,n);if(t.response_format!==void 0){const n=t.response_format;if(!n||typeof n!="object"||Array.isArray(n)||!["text","json_object","json_schema"].includes(n.type)||n.type==="json_schema"&&(typeof n.json_schema?.name!="string"||!n.json_schema.name||!n.json_schema.schema||typeof n.json_schema.schema!="object"||Array.isArray(n.json_schema.schema)))throw new s(400,"invalid_request","invalid response_format","response_format")}if(t.metadata!==void 0&&Buffer.byteLength(JSON.stringify(t.metadata)??"")>4096)throw new s(400,"invalid_request","metadata exceeds 4 KB","metadata");return t}c(qt,"parseLlmRequest");function Qe(e){try{const t=JSON.parse(e);return t!==null&&typeof t=="object"&&!Array.isArray(t)}catch{return!1}}c(Qe,"isJsonObject");function Ve(e){const t=Date.now(),n=e.messages.map(r=>r.role==="system"?{role:"system",content:r.content??"",timestamp:t}:r.role==="tool"?{role:"toolResult",toolCallId:r.tool_call_id??"",toolName:"",content:typeof r.content=="string"||r.content===null?[{type:"text",text:r.content??""}]:be(r.content),isError:!1,timestamp:t}:r.role==="assistant"?{role:"assistant",content:[...r.content?[{type:"text",text:r.content}]:[],...(r.tool_calls??[]).map(_=>({type:"toolCall",id:_.id,name:_.function.name,arguments:JSON.parse(_.function.arguments)}))],api:"openai-completions",provider:"openai",model:e.model??"",usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:"stop",timestamp:t}:{role:"user",content:be(r.content),timestamp:t}),i=n.findIndex(r=>r.role!=="system"),d=n.slice(0,i<0?n.length:i).map(r=>r.content),y=d.join(`
|
|
2
2
|
|
|
3
|
-
`),s=e.tools?.map(({function:a})=>({name:a.name,description:a.description??"",parameters:a.parameters??{type:"object",properties:{}}}));return{..._?{systemPrompt:_}:{},messages:n.slice(k.length),...s?{tools:s}:{}}}u(Je,"contextOf");function me(e,t,n,r){const k=u(()=>{const s=ae(n,r),a=Object.values({...s.modelRoutes??{},...s.legacyModelRoutes??{}});return a.length?[s.modelLadders.defaultProvider,...s.modelRouting?.familyOrder??[],...a.map(g=>g.family)].find(g=>a.some(E=>E.family===g)):s.modelLadders.defaultProvider??"anthropic"},"ownerFamily");if(!e){const s=k();return{kind:"logical",family:s,strength:"strong",spec:`${s}/strong`}}const _=/^([^/]+)\/(ultra|strong|medium|light)$/.exec(e);if(_){if(_[1]===xe){const s=k();return{kind:"logical",family:s,strength:_[2],spec:`${s}/${_[2]}`}}return{kind:"logical",family:_[1],strength:_[2],spec:e}}return{kind:t==="exact"?"exact":"concrete",spec:e}}u(me,"requestFor");function We(e,t){const{input:n,output:r,cacheRead:k,cacheWrite:_}=e.cost;return n===0&&r===0&&k===0&&_===0?null:Number.isFinite(t)&&t>=0?t:null}u(We,"usdEquivalent");function He(e,t){return e.compat?.allowedFallbackModels?.find(r=>r.provider===e.provider&&r.model===t)}u(He,"allowedFallback");function ut(e){return e?{payer:e.payer??"user",sub:e.sub,grantee:e.grantee,inference_endpoint:e.inference_endpoint??null}:void 0}u(ut,"llmCallerOf");function Ke(e){return e instanceof Ne?e.code==="server_error"?new i(503,"upstream_model_unavailable",e.message):new i(404,"model_unavailable",e.message):e}u(Ke,"mintFailure");const ze=/\bmodel is not supported when using Codex with a ChatGPT account\b/i;function Be(e,t,n){if(!(e!=="openai-codex"||t===void 0||!ze.test(t)))return new i(404,"model_unavailable",t,void 0,{served_model:n})}u(Be,"codexAccountModelRejection");async function ft(e,t,n,r,k,_){const s=_?.payer==="app"?_:void 0;if(e.payer==="app"&&!s)throw new i(403,"payer_not_app","this grant is not app-paid");if(e.payer==="user"&&s)throw new i(400,"invalid_request","this grant is app-paid; payer cannot be user","payer");if(s&&!s.inference_endpoint)throw new i(404,"model_unavailable",`${s.grantee} is app-paid but its grant carries no inference endpoint`);const a=s?.inference_endpoint??void 0;s||await Le(k);const g=await Me(),E=u(c=>c.routeId!=="concrete"&&c.routeId!=="exact","configured"),ee=u(c=>{const h=g.find(c.providerId,c.modelId);return h!==void 0&&(!e.reasoning_effort||h.reasoning)&&(e.seed===void 0&&e.response_format===void 0||h.api==="openai-completions")&&(e.stop===void 0||["openai-completions","anthropic-messages"].includes(h.api))},"matchesFeatures"),pe=u(()=>{const c=me(e.model,e.model_policy,r?.cwd,r?.profileId),h=ie(c.kind==="exact"?{kind:"concrete",spec:c.spec}:c,r?.cwd,r?.profileId).filter(E);let d;if(c.kind==="logical")d=e.model_policy==="exact"?h.filter(m=>m.family===c.family&&m.strength===c.strength).slice(0,1):h;else{const m=se(c.spec),q=h.filter(M=>M.providerId===m?.providerId&&M.modelId===m.modelId&&(e.model_policy!=="exact"||M.thinkingLevel===m.thinkingLevel)&&ee(M));if(e.model_policy==="exact")d=q.slice(0,1);else if(q.length)d=q;else{const M=ae(r?.cwd,r?.profileId),w=[...Object.values(M.modelRoutes??{}).flatMap(v=>Object.entries(v.models)),...[M.modelLadders.anthropic,M.modelLadders.openai].flatMap(v=>Object.entries(v))].find(([,v])=>v===c.spec)?.[0],f=me(void 0,"substitute",r?.cwd,r?.profileId);d=w&&f.kind==="logical"?ie({...f,strength:w,spec:c.spec},r?.cwd,r?.profileId).filter(v=>E(v)&&v.strength===w):[]}}return d},"ownerCandidates");let R;if(a?R=De(a,e.model,e.model_policy==="exact").flatMap((c,h)=>{const d=se(c.model);return d?[{routeId:"app",credentialSource:"app",family:"app",strength:c.strength,...d,rank:h}]:[]}):R=pe(),R=R.filter(ee),R.length===0)throw e.model_policy==="exact"?new i(400,"substitution_refused","no eligible model route is configured"):new i(404,"model_unavailable","no eligible model route is configured");const te=`chatcmpl-${ve()}`;let I=1/0,b,z;for(const c of R){if(!s&&!ke(c,g).authenticated)continue;const h=g.find(c.providerId,c.modelId);if(!h)continue;const d=a?{...h,baseUrl:`${a.base_url}/${h.provider}`}:h,m=c.providerId,q=!s&&H(m)?qe(m).filter(w=>!w.authFailure):[],M=q.filter(w=>le(w,d.id)<=Date.now());if(q.length&&!M.length){const w=Math.min(...q.map(f=>le(f,d.id)));w<I&&(I=w,z=`${d.provider}/${d.id}`);continue}const oe=s?[void 0,void 0]:q.length?M:[void 0];for(const[w,f]of oe.entries()){const v=Date.now();let ne=f?.access;if(f&&f.expires<=Date.now()&&H(m)){const o=g.getProvider(m)?.auth.oauth;if(!o)throw new i(503,"upstream_model_unavailable",`no OAuth refresh for ${m}`,void 0,{served_model:`${d.provider}/${d.id}`});ne=(await Re(m,f.label,async(x,re)=>{const W=await o.refresh({type:"oauth",refresh:x,access:"",expires:0,...re?{clientId:re}:{}},t??new AbortController().signal).catch(ge=>{throw Te(ge,`${d.provider}/${d.id}`)}),Q=W.clientId;return{refresh:W.refresh,access:W.access,expires:W.expires,...typeof Q=="string"&&Q?{clientId:Q}:{}}})).access}const he=Je(e);let F;if(s&&a)try{F=await Ee({audience:a.base_url,sub:s.sub,grantee:s.grantee,initiating_app:s.grantee,scope:["crtr:llm"]},{signal:t})}catch(o){throw Ke(o)}const C=f||s?void 0:await g.getApiKeyAndHeaders(d);if(C&&!C.ok)continue;const p=`${d.provider}/${d.id}`,S=e.model??p;let A=!1,L,j=3e4,$;const B=[],X=[],N=[];let l,D;const P=u((o,y=null)=>({id:te,object:"chat.completion.chunk",created:Math.floor(v/1e3),model:p,served_model:p,requested_model:S,substituted:S!==p&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test(S),choices:[{index:0,delta:o,finish_reason:y}]}),"chunk"),T=u(o=>{B.push(o),n?.(o)},"emit"),_e=we(d,he,{apiKey:F?.access_token??ne??(C?.ok?C.apiKey:void 0),...C?.ok?{headers:C.headers}:{},signal:t,reasoning:e.reasoning_effort??c.thinkingLevel,...e.temperature!==void 0?{temperature:e.temperature}:{},...e.max_tokens!==void 0?{maxTokens:e.max_tokens}:{},...e.top_p!==void 0?{samplingParams:{top_p:e.top_p}}:{},...e.tool_choice==="none"?{toolChoice:"none"}:{},onPayload:u(o=>{if(!o||typeof o!="object")return;const y=e.tool_choice,x=y==="required"?d.api==="anthropic-messages"?{type:"any"}:"required":typeof y=="object"&&y!==null?d.api==="anthropic-messages"?{type:"tool",name:y.function.name}:y:void 0;return{...o,...x?{tool_choice:x}:{},...e.stop!==void 0?{[d.api==="anthropic-messages"?"stop_sequences":"stop"]:typeof e.stop=="string"?[e.stop]:e.stop}:{},...e.seed!==void 0?{seed:e.seed}:{},...e.response_format!==void 0?{response_format:e.response_format}:{}}},"onPayload"),fetch:u(async(o,y)=>{const x=await fetch(o,y);return L=x.status,x.status===429&&(A=!0,$=x.headers,j=K(x.headers)??15e3),x},"fetch"),onResponse:u(o=>{o.status===429&&(A=!0,$=o.headers,j=K(o.headers)??15e3)},"onResponse")});try{for await(const o of _e)o.type==="text_delta"&&(X.push(o.delta),T(P({content:o.delta}))),o.type==="thinking_delta"&&(N.push(o.delta),T(P({reasoning_content:o.delta}))),o.type==="done"&&(l=o.message),o.type==="error"&&(l=o.error)}catch(o){const y=$e(o);$===void 0&&o&&typeof o=="object"&&"headers"in o&&Object.keys(Se(o.headers)).length&&($=o.headers);const x=V({status:o&&typeof o=="object"&&"status"in o?o.status:void 0,message:o instanceof Error?o.message:String(o)});if(s)D=o;else{if(!A&&x.kind!=="rate-limit")throw B.length?new i(503,"upstream_model_unavailable",o instanceof Error?o.message:String(o),void 0,{served_model:p}):o;A=!0,j=y??j}}if(s){if(L===401){if(F&&Fe(F.key),w===0)continue;throw new i(404,"model_unavailable",`${a.base_url} refuses this runtime's tokens`,void 0,{served_model:p})}if(A&&K($)===void 0)throw new i(503,"upstream_model_unavailable",`${p} was rate-limited without a retry-after`,void 0,{served_model:p});if(!A&&(D!==void 0||L!==void 0&&L>=500))throw new i(503,"upstream_model_unavailable",D instanceof Error?D.message:l?.errorMessage??"inference endpoint failed",void 0,{served_model:p})}if(!A&&l?.stopReason==="error"&&V({message:l.errorMessage}).kind==="rate-limit"&&(A=!0,j=15e3),A){const o=Ce($,d.id);if(K($)===void 0&&o.resetAt!==void 0&&(j=Math.max(j,o.resetAt-Date.now())),b={provider:m,model:d.id,...f?{account:f.label}:{},observation:o},console.error(`[llm] 429 from ${p}${f?` account "${f.label}"`:""}: scope=${ce(o.scope)} retry_after_ms=${j} headers=${JSON.stringify(o.headers)}`),f&&H(m)&&await je(m,f.label,j,v,Date.now(),o.scope),Date.now()+Math.min(j,de)<I&&(I=Date.now()+Math.min(j,de),z=p),X.length||N.length)throw new i(503,"service_unavailable","vendor rate limit after output began",void 0,{served_model:p});if(s)break;continue}const U=Oe(l);if(L===400&&e.tool_choice!==void 0&&e.tool_choice!=="auto"&&e.tool_choice!=="none"&&/\btool[_ ]choice\b/i.test(l?.errorMessage??"")&&/not supported|does not support|unsupported|cannot|must be auto/i.test(l?.errorMessage??""))throw new i(400,"invalid_request",l.errorMessage,"tool_choice",{served_model:p});if(l?.stopReason==="error"){const o=Be(d.provider,l.errorMessage,p);if(o)throw o}if(!l||l.stopReason==="error"&&!U||l.stopReason==="aborted")throw new i(503,"upstream_model_unavailable",l?.errorMessage??"model request failed",void 0,{served_model:p});f&&H(m)&&Ie(f,d.id)&&await Ae(m,f.label,v,d.id);const G=l.content.filter(o=>o.type==="toolCall").map(o=>({id:o.id,type:"function",function:{name:o.name,arguments:JSON.stringify(o.arguments)}})),Y=U?"content_filter":l.stopReason==="toolUse"?"tool_calls":l.stopReason==="length"?"length":"stop",Z=l.responseModel?He(d,l.responseModel):void 0,O=Z?`${d.provider}/${Z.model}`:p,J={prompt_tokens:l.usage.input,completion_tokens:l.usage.output,total_tokens:l.usage.totalTokens,cache_read_tokens:l.usage.cacheRead,cache_write_tokens:l.usage.cacheWrite,usd_equivalent:We(Z??d,l.usage.cost.total),served_model:O};s?await ue(s.grantee,"app",O,l.usage,J.usd_equivalent):k&&await ue(k.grantee,"user",O,l.usage,J.usd_equivalent);const ye={id:te,object:"chat.completion",created:Math.floor(v/1e3),model:O,served_model:O,requested_model:S,substituted:S!==O&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test(S),choices:[{index:0,message:{role:"assistant",content:X.join("")||null,...N.length?{reasoning_content:N.join("")}:{},...G.length?{tool_calls:G}:{},...U?{refusal:l.errorMessage??"Content filtered"}:{}},finish_reason:Y}],usage:J,...e.metadata!==void 0?{metadata:e.metadata}:{}};for(const[o,y]of G.entries())T(P({tool_calls:[{index:o,...y}]}));return T({...P({},Y),...U?{choices:[{index:0,delta:{},finish_reason:Y,message:{refusal:l.errorMessage??"Content filtered"}}]}:{},usage:J}),{completion:ye,chunks:B}}}throw I!==1/0?new i(429,"all_routes_exhausted","all eligible model routes are rate-limited",void 0,{retry_after_s:Math.max(1,Math.ceil((I-Date.now())/1e3)),served_model:b?`${b.provider}/${b.model}`:z,...b?{rate_limit:{provider:b.provider,model:b.model,...b.account?{account:b.account}:{},scope:ce(b.observation.scope),...b.observation.claim?{claim:b.observation.claim}:{},headers:b.observation.headers}}:{}},void 0,!0):e.model_policy==="exact"?new i(400,"substitution_refused","the requested model is unavailable"):new i(404,"model_unavailable","no usable model route is available")}u(ft,"runLlm");export{ut as llmCallerOf,ct as parseLlmRequest,ft as runLlm};
|
|
3
|
+
`),h=e.tools?.map(({function:r})=>({name:r.name,description:r.description??"",parameters:r.parameters??{type:"object",properties:{}}}));return{...y?{systemPrompt:y}:{},messages:n.slice(d.length),...h?{tools:h}:{}}}c(Ve,"contextOf");function ve(e,t,n,i){const d=c(()=>{const h=me(n,i),r=Object.values({...h.modelRoutes??{},...h.legacyModelRoutes??{}});return r.length?[h.modelLadders.defaultProvider,...h.modelRouting?.familyOrder??[],...r.map(_=>_.family)].find(_=>r.some($=>$.family===_)):h.modelLadders.defaultProvider??"anthropic"},"ownerFamily");if(!e){const h=d();return{kind:"logical",family:h,strength:"strong",spec:`${h}/strong`}}const y=/^([^/]+)\/(ultra|strong|medium|light)$/.exec(e);if(y){if(y[1]===Ie){const h=d();return{kind:"logical",family:h,strength:y[2],spec:`${h}/${y[2]}`}}return{kind:"logical",family:y[1],strength:y[2],spec:e}}return{kind:t==="exact"?"exact":"concrete",spec:e}}c(ve,"requestFor");function et(e,t){const{input:n,output:i,cacheRead:d,cacheWrite:y}=e.cost;return n===0&&i===0&&d===0&&y===0?null:Number.isFinite(t)&&t>=0?t:null}c(et,"usdEquivalent");function tt(e,t){return e.compat?.allowedFallbackModels?.find(i=>i.provider===e.provider&&i.model===t)}c(tt,"allowedFallback");function Mt(e){return e?{payer:e.payer??"user",sub:e.sub,grantee:e.grantee,inference_endpoint:e.inference_endpoint??null}:void 0}c(Mt,"llmCallerOf");function ot(e){return e instanceof Xe?e.code==="server_error"?new s(503,"upstream_model_unavailable",e.message):new s(404,"model_unavailable",e.message):e}c(ot,"mintFailure");const nt=/\bmodel is not supported when using Codex with a ChatGPT account\b/i;function rt(e,t,n){if(!(e!=="openai-codex"||t===void 0||!nt.test(t)))return new s(404,"model_unavailable",t,void 0,{served_model:n})}c(rt,"codexAccountModelRejection");function it(e){return e.get("request-id")??e.get("x-request-id")}c(it,"vendorRequestId");async function jt(e,t,n,i,d,y,h){const r=y?.payer==="app"?y:void 0;if(e.payer==="app"&&!r)throw new s(403,"payer_not_app","this grant is not app-paid");if(e.payer==="user"&&r)throw new s(400,"invalid_request","this grant is app-paid; payer cannot be user","payer");if(r&&!r.inference_endpoint)throw new s(404,"model_unavailable",`${r.grantee} is app-paid but its grant carries no inference endpoint`);const _=r?.inference_endpoint??void 0,$=d===void 0?null:r?.sub??d.sub??await He(),E=await Oe(),ie=c(u=>u.routeId!=="concrete"&&u.routeId!=="exact","configured"),se=c(u=>{const g=E.find(u.providerId,u.modelId);return g!==void 0&&(!e.reasoning_effort||g.reasoning)&&(e.seed===void 0&&e.response_format===void 0||g.api==="openai-completions")&&(e.stop===void 0||["openai-completions","anthropic-messages"].includes(g.api))},"matchesFeatures"),xe=c(()=>{const u=ve(e.model,e.model_policy,i?.cwd,i?.profileId),g=ue(u.kind==="exact"?{kind:"concrete",spec:u.spec}:u,i?.cwd,i?.profileId).filter(ie);let a;if(u.kind==="logical")a=e.model_policy==="exact"?g.filter(f=>f.family===u.family&&f.strength===u.strength).slice(0,1):g;else{const f=fe(u.spec),A=g.filter(q=>q.providerId===f?.providerId&&q.modelId===f.modelId&&(e.model_policy!=="exact"||q.thinkingLevel===f.thinkingLevel)&&se(q));if(e.model_policy==="exact")a=A.slice(0,1);else if(A.length)a=A;else{const q=me(i?.cwd,i?.profileId),k=[...Object.values(q.modelRoutes??{}).flatMap(v=>Object.entries(v.models)),...[q.modelLadders.anthropic,q.modelLadders.openai].flatMap(v=>Object.entries(v))].find(([,v])=>v===u.spec)?.[0],m=ve(void 0,"substitute",i?.cwd,i?.profileId);a=k&&m.kind==="logical"?ue({...m,strength:k,spec:u.spec},i?.cwd,i?.profileId).filter(v=>ie(v)&&v.strength===k):[]}}return a},"ownerCandidates");let C;if(_?C=Ge(_,e.model,e.model_policy==="exact").flatMap((u,g)=>{const a=fe(u.model);return a?[{routeId:"app",credentialSource:"app",family:"app",strength:u.strength,...a,rank:g}]:[]}):C=xe(),C=C.filter(se),C.length===0)throw e.model_policy==="exact"?new s(400,"substitution_refused","no eligible model route is configured"):new s(404,"model_unavailable","no eligible model route is configured");const D=`chatcmpl-${Re()}`;let O=1/0,b,X,G;const ae=c(async()=>h??(d?.node?(await we(d.node))?.caused_by??null:null)??(d?.run?(await we(d.run))?.caused_by??null:null),"causeOf");for(const u of C){if(!r&&!Ce(u,E).authenticated)continue;const g=E.find(u.providerId,u.modelId);if(!g)continue;const a=_?{...g,baseUrl:`${_.base_url}/${g.provider}`}:g,f=u.providerId,A=!r&&K(f)?Ee(f).filter(k=>!k.authFailure):[],q=A.filter(k=>he(k,a.id)<=Date.now());if(A.length&&!q.length){const k=Math.min(...A.map(m=>he(m,a.id)));k<O&&(O=k,X=`${a.provider}/${a.id}`);continue}const le=r?[void 0,void 0]:A.length?q:[void 0];for(const[k,m]of le.entries()){const v=Date.now();let de=m?.access;if(m&&m.expires<=Date.now()&&K(f)){const o=E.getProvider(f)?.auth.oauth;if(!o)throw new s(503,"upstream_model_unavailable",`no OAuth refresh for ${f}`,void 0,{served_model:`${a.provider}/${a.id}`});de=(await Ne(f,m.label,async(x,ce)=>{const H=await o.refresh({type:"oauth",refresh:x,access:"",expires:0,...ce?{clientId:ce}:{}},t??new AbortController().signal).catch(je=>{throw Ze(je,`${a.provider}/${a.id}`)}),ne=H.clientId;return{refresh:H.refresh,access:H.access,expires:H.expires,...typeof ne=="string"&&ne?{clientId:ne}:{}}})).access}const ke=Ve(e),S=m||r?void 0:await E.getApiKeyAndHeaders(a);if(S&&!S.ok)continue;const p=`${a.provider}/${a.id}`,L=e.model??p,R=r?ge({call:"model",route:"endpoint",grantee:r.grantee}):$===null?null:ge({call:"model",route:m?"subscription":"key",sub:$});if(d!==void 0&&R!==null){const o=await We({scope:"call",payer:R.payer,funding:R.funding,sub:$,grantee:d.grantee,provider:f});if(o!==null){await ye({request:d.request??D,kind:"model",what:p,usd:null,payer:R.payer,funding:R.funding,sub:$,grantee:d.grantee,node:d.node??null,run:d.run??null,provider:f,causedBy:await ae(),refused:Be(o)}),G??=o;break}}let P;if(r&&_)try{P=await Ke({audience:_.base_url,sub:r.sub,grantee:r.grantee,initiating_app:r.grantee,scope:["crtr:llm"]},{signal:t})}catch(o){throw ot(o)}let j=!1,F,Y=null,M=3e4,I;const Z=[],Q=[],T=[];let l,J;const W=c((o,w=null)=>({id:D,object:"chat.completion.chunk",created:Math.floor(v/1e3),model:p,served_model:p,requested_model:L,substituted:L!==p&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test(L),choices:[{index:0,delta:o,finish_reason:w}]}),"chunk"),U=c(o=>{Z.push(o),n?.(o)},"emit"),qe=$e(a,ke,{apiKey:P?.access_token??de??(S?.ok?S.apiKey:void 0),...S?.ok?{headers:S.headers}:{},signal:t,reasoning:e.reasoning_effort??u.thinkingLevel,...e.temperature!==void 0?{temperature:e.temperature}:{},...e.max_tokens!==void 0?{maxTokens:e.max_tokens}:{},...e.top_p!==void 0?{samplingParams:{top_p:e.top_p}}:{},...e.tool_choice==="none"?{toolChoice:"none"}:{},onPayload:c(o=>{if(!o||typeof o!="object")return;const w=e.tool_choice,x=w==="required"?a.api==="anthropic-messages"?{type:"any"}:"required":typeof w=="object"&&w!==null?a.api==="anthropic-messages"?{type:"tool",name:w.function.name}:w:void 0;return{...o,...x?{tool_choice:x}:{},...e.stop!==void 0?{[a.api==="anthropic-messages"?"stop_sequences":"stop"]:typeof e.stop=="string"?[e.stop]:e.stop}:{},...e.seed!==void 0?{seed:e.seed}:{},...e.response_format!==void 0?{response_format:e.response_format}:{}}},"onPayload"),fetch:c(async(o,w)=>{const x=await fetch(o,w);return F=x.status,Y=it(x.headers)??Y,x.status===429&&(j=!0,I=x.headers,M=z(x.headers)??15e3),x},"fetch"),onResponse:c(o=>{o.status===429&&(j=!0,I=o.headers,M=z(o.headers)??15e3)},"onResponse")});try{for await(const o of qe)o.type==="text_delta"&&(Q.push(o.delta),U(W({content:o.delta}))),o.type==="thinking_delta"&&(T.push(o.delta),U(W({reasoning_content:o.delta}))),o.type==="done"&&(l=o.message),o.type==="error"&&(l=o.error)}catch(o){const w=Fe(o);I===void 0&&o&&typeof o=="object"&&"headers"in o&&Object.keys(Te(o.headers)).length&&(I=o.headers);const x=re({status:o&&typeof o=="object"&&"status"in o?o.status:void 0,message:o instanceof Error?o.message:String(o)});if(r)J=o;else{if(!j&&x.kind!=="rate-limit")throw Z.length?new s(503,"upstream_model_unavailable",o instanceof Error?o.message:String(o),void 0,{served_model:p}):o;j=!0,M=w??M}}if(r){if(F===401){if(P&&ze(P.key),k===0)continue;throw new s(404,"model_unavailable",`${_.base_url} refuses this runtime's tokens`,void 0,{served_model:p})}if(j&&z(I)===void 0)throw new s(503,"upstream_model_unavailable",`${p} was rate-limited without a retry-after`,void 0,{served_model:p});if(!j&&(J!==void 0||F!==void 0&&F>=500))throw new s(503,"upstream_model_unavailable",J instanceof Error?J.message:l?.errorMessage??"inference endpoint failed",void 0,{served_model:p})}if(!j&&l?.stopReason==="error"&&re({message:l.errorMessage}).kind==="rate-limit"&&(j=!0,M=15e3),j){const o=Pe(I,a.id);if(z(I)===void 0&&o.resetAt!==void 0&&(M=Math.max(M,o.resetAt-Date.now())),b={provider:f,model:a.id,...m?{account:m.label}:{},observation:o},console.error(`[llm] 429 from ${p}${m?` account "${m.label}"`:""}: scope=${_e(o.scope)} retry_after_ms=${M} headers=${JSON.stringify(o.headers)}`),m&&K(f)&&await Se(f,m.label,M,v,Date.now(),o.scope),Date.now()+Math.min(M,pe)<O&&(O=Date.now()+Math.min(M,pe),X=p),Q.length||T.length)throw new s(503,"service_unavailable","vendor rate limit after output began",void 0,{served_model:p});if(r)break;continue}const B=Je(l);if(F===400&&e.tool_choice!==void 0&&e.tool_choice!=="auto"&&e.tool_choice!=="none"&&/\btool[_ ]choice\b/i.test(l?.errorMessage??"")&&/not supported|does not support|unsupported|cannot|must be auto/i.test(l?.errorMessage??""))throw new s(400,"invalid_request",l.errorMessage,"tool_choice",{served_model:p});if(l?.stopReason==="error"){const o=rt(a.provider,l.errorMessage,p);if(o)throw o}if(!l||l.stopReason==="error"&&!B||l.stopReason==="aborted")throw new s(503,"upstream_model_unavailable",l?.errorMessage??"model request failed",void 0,{served_model:p});m&&K(f)&&De(m,a.id)&&await Le(f,m.label,v,a.id);const V=l.content.filter(o=>o.type==="toolCall").map(o=>({id:o.id,type:"function",function:{name:o.name,arguments:JSON.stringify(o.arguments)}})),ee=B?"content_filter":l.stopReason==="toolUse"?"tool_calls":l.stopReason==="length"?"length":"stop",te=l.responseModel?tt(a,l.responseModel):void 0,N=te?`${a.provider}/${te.model}`:p,oe={prompt_tokens:l.usage.input,completion_tokens:l.usage.output,total_tokens:l.usage.totalTokens,cache_read_tokens:l.usage.cacheRead,cache_write_tokens:l.usage.cacheWrite,usd_equivalent:et(te??a,l.usage.cost.total),served_model:N};d!==void 0&&R!==null&&await ye({request:d.request??D,kind:"model",what:N,units:{input:l.usage.input,output:l.usage.output,cache_read:l.usage.cacheRead,cache_write:l.usage.cacheWrite},usd:oe.usd_equivalent,payer:R.payer,funding:R.funding,sub:$,grantee:d.grantee,node:d.node??null,run:d.run??null,provider:f,causedBy:await ae(),vendorRef:Y});const Me={id:D,object:"chat.completion",created:Math.floor(v/1e3),model:N,served_model:N,requested_model:L,substituted:L!==N&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test(L),choices:[{index:0,message:{role:"assistant",content:Q.join("")||null,...T.length?{reasoning_content:T.join("")}:{},...V.length?{tool_calls:V}:{},...B?{refusal:l.errorMessage??"Content filtered"}:{}},finish_reason:ee}],usage:oe,...e.metadata!==void 0?{metadata:e.metadata}:{}};for(const[o,w]of V.entries())U(W({tool_calls:[{index:o,...w}]}));return U({...W({},ee),...B?{choices:[{index:0,delta:{},finish_reason:ee,message:{refusal:l.errorMessage??"Content filtered"}}]}:{},usage:oe}),{completion:Me,chunks:Z}}}throw G!==void 0?Ue(G):O!==1/0?new s(429,"all_routes_exhausted","all eligible model routes are rate-limited",void 0,{retry_after_s:Math.max(1,Math.ceil((O-Date.now())/1e3)),served_model:b?`${b.provider}/${b.model}`:X,...b?{rate_limit:{provider:b.provider,model:b.model,...b.account?{account:b.account}:{},scope:_e(b.observation.scope),...b.observation.claim?{claim:b.observation.claim}:{},headers:b.observation.headers}}:{}},void 0,!0):e.model_policy==="exact"?new s(400,"substitution_refused","the requested model is unavailable"):new s(404,"model_unavailable","no usable model route is available")}c(jt,"runLlm");export{Mt as llmCallerOf,qt as parseLlmRequest,jt as runLlm};
|
|
@@ -2,6 +2,7 @@ import type { ManifestHttpLeaf, ManifestBranch, ManifestRootEntry } from '../../
|
|
|
2
2
|
import type { ValidatedCommandManifest } from '../../api/command-manifest/manifest.js';
|
|
3
3
|
import type { ProviderCaller, ProviderCallInput, ProviderRequest } from '../../daemon/api/handlers/providers.js';
|
|
4
4
|
import { ProviderError } from './errors.js';
|
|
5
|
+
import type { Funding } from '../ledger/types.js';
|
|
5
6
|
/** `tool` is a canonical command path, relative to the provider and never provider-prefixed. `hidden` is true for a leaf declared hidden or under a hidden branch: it stays callable by its exact path but is never listed. */
|
|
6
7
|
export interface ProviderLeaf {
|
|
7
8
|
tool: string;
|
|
@@ -58,8 +59,13 @@ export interface ProviderResponseContext {
|
|
|
58
59
|
grantee: string;
|
|
59
60
|
/** The outbound token's payer: the principal the directory charges for this call. */
|
|
60
61
|
payer: string;
|
|
61
|
-
/**
|
|
62
|
+
/** The app the charge is attributed to: the calling app, or `app:terminal` for the person (whose outbound identity is their sub). */
|
|
62
63
|
ledger_grantee: string;
|
|
64
|
+
/** `bill` when the person pays, `endpoint` when the app pays (`payerOf`). */
|
|
65
|
+
funding: Funding;
|
|
66
|
+
/** The calling node, and the `Crtr-Caused-By` the call arrived with; null when none. */
|
|
67
|
+
node_id: string | null;
|
|
68
|
+
caused_by: string | null;
|
|
63
69
|
jti: string;
|
|
64
70
|
status: number;
|
|
65
71
|
headers: Record<string, string | string[] | undefined>;
|