@granular-software/sdk 0.4.37 → 0.4.38

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/cli/index.js CHANGED
@@ -19590,6 +19590,7 @@ function withPromptTranscriptTimeout(promise) {
19590
19590
  var Session = class {
19591
19591
  client;
19592
19592
  clientId;
19593
+ initialQuota;
19593
19594
  jobsMap = /* @__PURE__ */ new Map();
19594
19595
  pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
19595
19596
  eventListeners = /* @__PURE__ */ new Map();
@@ -19605,9 +19606,10 @@ var Session = class {
19605
19606
  promptCache = /* @__PURE__ */ new Map();
19606
19607
  /** Prompt ids locally answered before the document sync catches up. */
19607
19608
  hiddenPromptIds = /* @__PURE__ */ new Set();
19608
- constructor(client, clientId) {
19609
+ constructor(client, clientId, options = {}) {
19609
19610
  this.client = client;
19610
19611
  this.clientId = clientId || `client_${Date.now()}`;
19612
+ this.initialQuota = options.initialQuota || null;
19611
19613
  this.setupEventHandlers();
19612
19614
  this.setupToolInvokeHandler();
19613
19615
  }
@@ -19656,6 +19658,16 @@ var Session = class {
19656
19658
  get document() {
19657
19659
  return this.client.doc;
19658
19660
  }
19661
+ get quota() {
19662
+ return this.getQuota();
19663
+ }
19664
+ getQuota() {
19665
+ const quota = this.client.doc.billing?.quota;
19666
+ if (quota && typeof quota === "object") {
19667
+ return quota;
19668
+ }
19669
+ return this.initialQuota;
19670
+ }
19659
19671
  get sessionId() {
19660
19672
  return this.client.currentSessionId;
19661
19673
  }
@@ -21511,6 +21523,110 @@ async function invokeRegisteredEffect(effectMap, request) {
21511
21523
  return resolved.handler(request.input, context);
21512
21524
  }
21513
21525
 
21526
+ // src/spend.ts
21527
+ function toGranularHttpBase(apiUrl) {
21528
+ const url = new URL(apiUrl);
21529
+ if (url.protocol === "ws:") {
21530
+ url.protocol = "http:";
21531
+ } else if (url.protocol === "wss:") {
21532
+ url.protocol = "https:";
21533
+ }
21534
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
21535
+ if (!url.pathname || url.pathname === "/") {
21536
+ url.pathname = "/granular";
21537
+ }
21538
+ url.search = "";
21539
+ url.hash = "";
21540
+ return url.toString().replace(/\/$/, "");
21541
+ }
21542
+ function cleanIdPart(value) {
21543
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
21544
+ }
21545
+ function buildOpenAISpendEventId(usage, context = {}) {
21546
+ const requestId = usage.requestId?.trim();
21547
+ if (!requestId) return void 0;
21548
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
21549
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
21550
+ }
21551
+ function pricingEffectiveAtSeconds(value) {
21552
+ if (!value) return null;
21553
+ const parsed = Date.parse(value);
21554
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
21555
+ }
21556
+ function compactContext(context) {
21557
+ return Object.fromEntries(
21558
+ Object.entries(context).filter(
21559
+ ([, value]) => value != null && value !== ""
21560
+ )
21561
+ );
21562
+ }
21563
+ function omitTenantId(context) {
21564
+ const scopedContext = { ...context };
21565
+ delete scopedContext.tenantId;
21566
+ return scopedContext;
21567
+ }
21568
+ async function recordOpenAIUsageSpend(options) {
21569
+ const usageContext = compactContext({
21570
+ ...options.usage.usageContext || {},
21571
+ ...options.context || {}
21572
+ });
21573
+ const context = omitTenantId(usageContext);
21574
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
21575
+ const metadata = {
21576
+ ...options.metadata || {},
21577
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
21578
+ usageContext: context
21579
+ };
21580
+ const response = await fetch(
21581
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
21582
+ {
21583
+ method: "POST",
21584
+ cache: "no-store",
21585
+ headers: {
21586
+ Authorization: `Bearer ${options.token}`,
21587
+ "Content-Type": "application/json"
21588
+ },
21589
+ body: JSON.stringify({
21590
+ ...spendEventId ? { spendEventId } : {},
21591
+ sandboxId: context.sandboxId || null,
21592
+ environmentId: context.environmentId || null,
21593
+ sessionId: context.sessionId || null,
21594
+ subjectId: context.subjectId || null,
21595
+ permissionProfileId: context.permissionProfileId || null,
21596
+ source: "openai",
21597
+ lineItemType: "llm_tokens",
21598
+ provider: options.usage.provider,
21599
+ model: options.usage.model,
21600
+ operation: options.usage.operation || "chat.completions",
21601
+ requestId: options.usage.requestId || null,
21602
+ inputTokens: options.usage.inputTokens,
21603
+ outputTokens: options.usage.outputTokens,
21604
+ cachedInputTokens: options.usage.cachedInputTokens,
21605
+ reasoningTokens: options.usage.reasoningTokens,
21606
+ quantity: options.usage.totalTokens,
21607
+ quantityUnit: "tokens",
21608
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
21609
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
21610
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
21611
+ amountMicros: options.usage.amountMicros,
21612
+ currency: options.usage.currency,
21613
+ pricingSource: options.usage.pricingSource,
21614
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
21615
+ options.usage.pricingEffectiveAt
21616
+ ),
21617
+ estimated: false,
21618
+ metadata
21619
+ })
21620
+ }
21621
+ );
21622
+ if (!response.ok) {
21623
+ throw new Error(
21624
+ `Granular spend event failed (${response.status}): ${await response.text()}`
21625
+ );
21626
+ }
21627
+ return response.json();
21628
+ }
21629
+
21514
21630
  // src/manifest-metamodels.ts
21515
21631
  function buildFieldMetamodelMutations(fieldPath, spec) {
21516
21632
  return DEFAULT_METAMODEL_PACKAGES.flatMap(
@@ -23012,7 +23128,7 @@ var EnvironmentSession = class extends Session {
23012
23128
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
23013
23129
  graphContainerStatus = null;
23014
23130
  constructor(client, environment, clientId, options = {}) {
23015
- super(client, clientId);
23131
+ super(client, clientId, { initialQuota: options.initialQuota });
23016
23132
  this.environment = environment;
23017
23133
  this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
23018
23134
  }
@@ -23811,6 +23927,15 @@ var Granular = class _Granular {
23811
23927
  const environment = this.bindEnvironmentHandle(envData);
23812
23928
  return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
23813
23929
  }
23930
+ async recordOpenAIUsageSpend(usage, context, options) {
23931
+ return recordOpenAIUsageSpend({
23932
+ apiUrl: this.apiUrl,
23933
+ token: this.apiKey,
23934
+ usage,
23935
+ context,
23936
+ metadata: options?.metadata
23937
+ });
23938
+ }
23814
23939
  /**
23815
23940
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
23816
23941
  * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
@@ -23965,7 +24090,8 @@ var Granular = class _Granular {
23965
24090
  const environmentSession = new EnvironmentSession(
23966
24091
  client,
23967
24092
  environment,
23968
- clientId
24093
+ clientId,
24094
+ { initialQuota: session2.quota || null }
23969
24095
  );
23970
24096
  await environmentSession.hello();
23971
24097
  return environmentSession;