@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/index.mjs CHANGED
@@ -4616,6 +4616,7 @@ function withPromptTranscriptTimeout(promise) {
4616
4616
  var Session = class {
4617
4617
  client;
4618
4618
  clientId;
4619
+ initialQuota;
4619
4620
  jobsMap = /* @__PURE__ */ new Map();
4620
4621
  pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
4621
4622
  eventListeners = /* @__PURE__ */ new Map();
@@ -4631,9 +4632,10 @@ var Session = class {
4631
4632
  promptCache = /* @__PURE__ */ new Map();
4632
4633
  /** Prompt ids locally answered before the document sync catches up. */
4633
4634
  hiddenPromptIds = /* @__PURE__ */ new Set();
4634
- constructor(client, clientId) {
4635
+ constructor(client, clientId, options = {}) {
4635
4636
  this.client = client;
4636
4637
  this.clientId = clientId || `client_${Date.now()}`;
4638
+ this.initialQuota = options.initialQuota || null;
4637
4639
  this.setupEventHandlers();
4638
4640
  this.setupToolInvokeHandler();
4639
4641
  }
@@ -4682,6 +4684,16 @@ var Session = class {
4682
4684
  get document() {
4683
4685
  return this.client.doc;
4684
4686
  }
4687
+ get quota() {
4688
+ return this.getQuota();
4689
+ }
4690
+ getQuota() {
4691
+ const quota = this.client.doc.billing?.quota;
4692
+ if (quota && typeof quota === "object") {
4693
+ return quota;
4694
+ }
4695
+ return this.initialQuota;
4696
+ }
4685
4697
  get sessionId() {
4686
4698
  return this.client.currentSessionId;
4687
4699
  }
@@ -11304,6 +11316,110 @@ async function invokeRegisteredEffect(effectMap, request) {
11304
11316
  return resolved.handler(request.input, context);
11305
11317
  }
11306
11318
 
11319
+ // src/spend.ts
11320
+ function toGranularHttpBase(apiUrl) {
11321
+ const url = new URL(apiUrl);
11322
+ if (url.protocol === "ws:") {
11323
+ url.protocol = "http:";
11324
+ } else if (url.protocol === "wss:") {
11325
+ url.protocol = "https:";
11326
+ }
11327
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
11328
+ if (!url.pathname || url.pathname === "/") {
11329
+ url.pathname = "/granular";
11330
+ }
11331
+ url.search = "";
11332
+ url.hash = "";
11333
+ return url.toString().replace(/\/$/, "");
11334
+ }
11335
+ function cleanIdPart(value) {
11336
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
11337
+ }
11338
+ function buildOpenAISpendEventId(usage, context = {}) {
11339
+ const requestId = usage.requestId?.trim();
11340
+ if (!requestId) return void 0;
11341
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
11342
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
11343
+ }
11344
+ function pricingEffectiveAtSeconds(value) {
11345
+ if (!value) return null;
11346
+ const parsed = Date.parse(value);
11347
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
11348
+ }
11349
+ function compactContext(context) {
11350
+ return Object.fromEntries(
11351
+ Object.entries(context).filter(
11352
+ ([, value]) => value != null && value !== ""
11353
+ )
11354
+ );
11355
+ }
11356
+ function omitTenantId(context) {
11357
+ const scopedContext = { ...context };
11358
+ delete scopedContext.tenantId;
11359
+ return scopedContext;
11360
+ }
11361
+ async function recordOpenAIUsageSpend(options) {
11362
+ const usageContext = compactContext({
11363
+ ...options.usage.usageContext || {},
11364
+ ...options.context || {}
11365
+ });
11366
+ const context = omitTenantId(usageContext);
11367
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
11368
+ const metadata = {
11369
+ ...options.metadata || {},
11370
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
11371
+ usageContext: context
11372
+ };
11373
+ const response = await fetch(
11374
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
11375
+ {
11376
+ method: "POST",
11377
+ cache: "no-store",
11378
+ headers: {
11379
+ Authorization: `Bearer ${options.token}`,
11380
+ "Content-Type": "application/json"
11381
+ },
11382
+ body: JSON.stringify({
11383
+ ...spendEventId ? { spendEventId } : {},
11384
+ sandboxId: context.sandboxId || null,
11385
+ environmentId: context.environmentId || null,
11386
+ sessionId: context.sessionId || null,
11387
+ subjectId: context.subjectId || null,
11388
+ permissionProfileId: context.permissionProfileId || null,
11389
+ source: "openai",
11390
+ lineItemType: "llm_tokens",
11391
+ provider: options.usage.provider,
11392
+ model: options.usage.model,
11393
+ operation: options.usage.operation || "chat.completions",
11394
+ requestId: options.usage.requestId || null,
11395
+ inputTokens: options.usage.inputTokens,
11396
+ outputTokens: options.usage.outputTokens,
11397
+ cachedInputTokens: options.usage.cachedInputTokens,
11398
+ reasoningTokens: options.usage.reasoningTokens,
11399
+ quantity: options.usage.totalTokens,
11400
+ quantityUnit: "tokens",
11401
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
11402
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
11403
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
11404
+ amountMicros: options.usage.amountMicros,
11405
+ currency: options.usage.currency,
11406
+ pricingSource: options.usage.pricingSource,
11407
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
11408
+ options.usage.pricingEffectiveAt
11409
+ ),
11410
+ estimated: false,
11411
+ metadata
11412
+ })
11413
+ }
11414
+ );
11415
+ if (!response.ok) {
11416
+ throw new Error(
11417
+ `Granular spend event failed (${response.status}): ${await response.text()}`
11418
+ );
11419
+ }
11420
+ return response.json();
11421
+ }
11422
+
11307
11423
  // ../metamodel-enum/src/index.ts
11308
11424
  function renderInlineStringUnion(values) {
11309
11425
  return values.map((value) => JSON.stringify(value)).join(" | ");
@@ -14086,7 +14202,7 @@ var EnvironmentSession = class extends Session {
14086
14202
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
14087
14203
  graphContainerStatus = null;
14088
14204
  constructor(client, environment, clientId, options = {}) {
14089
- super(client, clientId);
14205
+ super(client, clientId, { initialQuota: options.initialQuota });
14090
14206
  this.environment = environment;
14091
14207
  this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
14092
14208
  }
@@ -14885,6 +15001,15 @@ var Granular = class _Granular {
14885
15001
  const environment = this.bindEnvironmentHandle(envData);
14886
15002
  return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
14887
15003
  }
15004
+ async recordOpenAIUsageSpend(usage, context, options) {
15005
+ return recordOpenAIUsageSpend({
15006
+ apiUrl: this.apiUrl,
15007
+ token: this.apiKey,
15008
+ usage,
15009
+ context,
15010
+ metadata: options?.metadata
15011
+ });
15012
+ }
14888
15013
  /**
14889
15014
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
14890
15015
  * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
@@ -15039,7 +15164,8 @@ var Granular = class _Granular {
15039
15164
  const environmentSession = new EnvironmentSession(
15040
15165
  client,
15041
15166
  environment,
15042
- clientId
15167
+ clientId,
15168
+ { initialQuota: session.quota || null }
15043
15169
  );
15044
15170
  await environmentSession.hello();
15045
15171
  return environmentSession;
@@ -17578,6 +17704,101 @@ ${knownFactsBlock}
17578
17704
  ${input.request?.trim() || "Use the latest user message in the conversation."}`;
17579
17705
  }
17580
17706
 
17581
- export { Environment, EnvironmentSession, Granular, OntologyHandle, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentReferentBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildSessionTranscript, consumeGranularReasoningOnlyChunk, consumeGranularReasoningTraceChunk, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizePrompt, normalizePromptChoiceOption, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch, stripGranularReasoningTrace };
17707
+ // src/openai-usage.ts
17708
+ var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
17709
+ var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
17710
+ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
17711
+ "gpt-5.4": {
17712
+ provider: "openai",
17713
+ model: "gpt-5.4",
17714
+ currency: "USD",
17715
+ inputUsdPerMillion: 2.5,
17716
+ cachedInputUsdPerMillion: 0.25,
17717
+ outputUsdPerMillion: 15,
17718
+ sourceUrl: OPENAI_PRICING_SOURCE_URL,
17719
+ effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
17720
+ }
17721
+ };
17722
+ function asRecord5(value) {
17723
+ return value && typeof value === "object" ? value : null;
17724
+ }
17725
+ function numberField(record, key) {
17726
+ const value = record?.[key];
17727
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17728
+ }
17729
+ function microsPerMillion(usdPerMillion) {
17730
+ return Math.round(usdPerMillion * 1e6);
17731
+ }
17732
+ function getOpenAIModelPricing(model) {
17733
+ return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
17734
+ }
17735
+ function normalizeOpenAIUsage(rawUsage) {
17736
+ const usage = asRecord5(rawUsage);
17737
+ if (!usage) {
17738
+ return {
17739
+ inputTokens: 0,
17740
+ cachedInputTokens: 0,
17741
+ uncachedInputTokens: 0,
17742
+ outputTokens: 0,
17743
+ reasoningTokens: 0,
17744
+ totalTokens: 0
17745
+ };
17746
+ }
17747
+ const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17748
+ const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17749
+ const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17750
+ const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17751
+ const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
17752
+ const cachedInputTokens = Math.min(
17753
+ inputTokens,
17754
+ numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
17755
+ );
17756
+ const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
17757
+ return {
17758
+ inputTokens,
17759
+ cachedInputTokens,
17760
+ uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
17761
+ outputTokens,
17762
+ reasoningTokens,
17763
+ totalTokens
17764
+ };
17765
+ }
17766
+ function calculateOpenAITokenSpend(model, rawUsage) {
17767
+ const pricing = getOpenAIModelPricing(model);
17768
+ if (!pricing) return null;
17769
+ const usage = normalizeOpenAIUsage(rawUsage);
17770
+ const inputPricePerMillionMicros = microsPerMillion(
17771
+ pricing.inputUsdPerMillion
17772
+ );
17773
+ const cachedInputPricePerMillionMicros = microsPerMillion(
17774
+ pricing.cachedInputUsdPerMillion
17775
+ );
17776
+ const outputPricePerMillionMicros = microsPerMillion(
17777
+ pricing.outputUsdPerMillion
17778
+ );
17779
+ const amountMicros = Math.round(
17780
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
17781
+ );
17782
+ return {
17783
+ provider: "openai",
17784
+ model,
17785
+ inputTokens: usage.inputTokens,
17786
+ cachedInputTokens: usage.cachedInputTokens,
17787
+ uncachedInputTokens: usage.uncachedInputTokens,
17788
+ outputTokens: usage.outputTokens,
17789
+ reasoningTokens: usage.reasoningTokens,
17790
+ totalTokens: usage.totalTokens,
17791
+ amountMicros,
17792
+ currency: "USD",
17793
+ inputPricePerMillionMicros,
17794
+ cachedInputPricePerMillionMicros,
17795
+ outputPricePerMillionMicros,
17796
+ pricingSource: pricing.sourceUrl,
17797
+ pricingEffectiveAt: pricing.effectiveDate,
17798
+ usage
17799
+ };
17800
+ }
17801
+
17802
+ export { Environment, EnvironmentSession, Granular, OPENAI_MODEL_PRICING_USD_PER_MILLION, OntologyHandle, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentReferentBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildOpenAISpendEventId, buildSessionTranscript, calculateOpenAITokenSpend, consumeGranularReasoningOnlyChunk, consumeGranularReasoningTraceChunk, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, getOpenAIModelPricing, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizeOpenAIUsage, normalizePrompt, normalizePromptChoiceOption, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, recordOpenAIUsageSpend, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch, stripGranularReasoningTrace, toGranularHttpBase };
17582
17803
  //# sourceMappingURL=index.mjs.map
17583
17804
  //# sourceMappingURL=index.mjs.map