@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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  var promises = require('fs/promises');
4
4
  var path = require('path');
5
+ var OpenAI = require('openai');
5
6
  var Automerge = require('@automerge/automerge');
6
7
 
7
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -25,6 +26,7 @@ function _interopNamespace(e) {
25
26
  }
26
27
 
27
28
  var path__default = /*#__PURE__*/_interopDefault(path);
29
+ var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
28
30
  var Automerge__namespace = /*#__PURE__*/_interopNamespace(Automerge);
29
31
 
30
32
  var __create = Object.create;
@@ -4643,6 +4645,7 @@ function withPromptTranscriptTimeout(promise) {
4643
4645
  var Session = class {
4644
4646
  client;
4645
4647
  clientId;
4648
+ initialQuota;
4646
4649
  jobsMap = /* @__PURE__ */ new Map();
4647
4650
  pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
4648
4651
  eventListeners = /* @__PURE__ */ new Map();
@@ -4658,9 +4661,10 @@ var Session = class {
4658
4661
  promptCache = /* @__PURE__ */ new Map();
4659
4662
  /** Prompt ids locally answered before the document sync catches up. */
4660
4663
  hiddenPromptIds = /* @__PURE__ */ new Set();
4661
- constructor(client, clientId) {
4664
+ constructor(client, clientId, options = {}) {
4662
4665
  this.client = client;
4663
4666
  this.clientId = clientId || `client_${Date.now()}`;
4667
+ this.initialQuota = options.initialQuota || null;
4664
4668
  this.setupEventHandlers();
4665
4669
  this.setupToolInvokeHandler();
4666
4670
  }
@@ -4709,6 +4713,16 @@ var Session = class {
4709
4713
  get document() {
4710
4714
  return this.client.doc;
4711
4715
  }
4716
+ get quota() {
4717
+ return this.getQuota();
4718
+ }
4719
+ getQuota() {
4720
+ const quota = this.client.doc.billing?.quota;
4721
+ if (quota && typeof quota === "object") {
4722
+ return quota;
4723
+ }
4724
+ return this.initialQuota;
4725
+ }
4712
4726
  get sessionId() {
4713
4727
  return this.client.currentSessionId;
4714
4728
  }
@@ -11331,6 +11345,110 @@ async function invokeRegisteredEffect(effectMap, request) {
11331
11345
  return resolved.handler(request.input, context);
11332
11346
  }
11333
11347
 
11348
+ // src/spend.ts
11349
+ function toGranularHttpBase(apiUrl) {
11350
+ const url = new URL(apiUrl);
11351
+ if (url.protocol === "ws:") {
11352
+ url.protocol = "http:";
11353
+ } else if (url.protocol === "wss:") {
11354
+ url.protocol = "https:";
11355
+ }
11356
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
11357
+ if (!url.pathname || url.pathname === "/") {
11358
+ url.pathname = "/granular";
11359
+ }
11360
+ url.search = "";
11361
+ url.hash = "";
11362
+ return url.toString().replace(/\/$/, "");
11363
+ }
11364
+ function cleanIdPart(value) {
11365
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
11366
+ }
11367
+ function buildOpenAISpendEventId(usage, context = {}) {
11368
+ const requestId = usage.requestId?.trim();
11369
+ if (!requestId) return void 0;
11370
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
11371
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
11372
+ }
11373
+ function pricingEffectiveAtSeconds(value) {
11374
+ if (!value) return null;
11375
+ const parsed = Date.parse(value);
11376
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
11377
+ }
11378
+ function compactContext(context) {
11379
+ return Object.fromEntries(
11380
+ Object.entries(context).filter(
11381
+ ([, value]) => value != null && value !== ""
11382
+ )
11383
+ );
11384
+ }
11385
+ function omitTenantId(context) {
11386
+ const scopedContext = { ...context };
11387
+ delete scopedContext.tenantId;
11388
+ return scopedContext;
11389
+ }
11390
+ async function recordOpenAIUsageSpend(options) {
11391
+ const usageContext = compactContext({
11392
+ ...options.usage.usageContext || {},
11393
+ ...options.context || {}
11394
+ });
11395
+ const context = omitTenantId(usageContext);
11396
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
11397
+ const metadata = {
11398
+ ...options.metadata || {},
11399
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
11400
+ usageContext: context
11401
+ };
11402
+ const response = await fetch(
11403
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
11404
+ {
11405
+ method: "POST",
11406
+ cache: "no-store",
11407
+ headers: {
11408
+ Authorization: `Bearer ${options.token}`,
11409
+ "Content-Type": "application/json"
11410
+ },
11411
+ body: JSON.stringify({
11412
+ ...spendEventId ? { spendEventId } : {},
11413
+ sandboxId: context.sandboxId || null,
11414
+ environmentId: context.environmentId || null,
11415
+ sessionId: context.sessionId || null,
11416
+ subjectId: context.subjectId || null,
11417
+ permissionProfileId: context.permissionProfileId || null,
11418
+ source: "openai",
11419
+ lineItemType: "llm_tokens",
11420
+ provider: options.usage.provider,
11421
+ model: options.usage.model,
11422
+ operation: options.usage.operation || "chat.completions",
11423
+ requestId: options.usage.requestId || null,
11424
+ inputTokens: options.usage.inputTokens,
11425
+ outputTokens: options.usage.outputTokens,
11426
+ cachedInputTokens: options.usage.cachedInputTokens,
11427
+ reasoningTokens: options.usage.reasoningTokens,
11428
+ quantity: options.usage.totalTokens,
11429
+ quantityUnit: "tokens",
11430
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
11431
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
11432
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
11433
+ amountMicros: options.usage.amountMicros,
11434
+ currency: options.usage.currency,
11435
+ pricingSource: options.usage.pricingSource,
11436
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
11437
+ options.usage.pricingEffectiveAt
11438
+ ),
11439
+ estimated: false,
11440
+ metadata
11441
+ })
11442
+ }
11443
+ );
11444
+ if (!response.ok) {
11445
+ throw new Error(
11446
+ `Granular spend event failed (${response.status}): ${await response.text()}`
11447
+ );
11448
+ }
11449
+ return response.json();
11450
+ }
11451
+
11334
11452
  // ../metamodel-enum/src/index.ts
11335
11453
  function renderInlineStringUnion(values) {
11336
11454
  return values.map((value) => JSON.stringify(value)).join(" | ");
@@ -14113,7 +14231,7 @@ var EnvironmentSession = class extends Session {
14113
14231
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
14114
14232
  graphContainerStatus = null;
14115
14233
  constructor(client, environment, clientId, options = {}) {
14116
- super(client, clientId);
14234
+ super(client, clientId, { initialQuota: options.initialQuota });
14117
14235
  this.environment = environment;
14118
14236
  this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
14119
14237
  }
@@ -14912,6 +15030,15 @@ var Granular = class _Granular {
14912
15030
  const environment = this.bindEnvironmentHandle(envData);
14913
15031
  return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
14914
15032
  }
15033
+ async recordOpenAIUsageSpend(usage, context, options) {
15034
+ return recordOpenAIUsageSpend({
15035
+ apiUrl: this.apiUrl,
15036
+ token: this.apiKey,
15037
+ usage,
15038
+ context,
15039
+ metadata: options?.metadata
15040
+ });
15041
+ }
14915
15042
  /**
14916
15043
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
14917
15044
  * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
@@ -15066,7 +15193,8 @@ var Granular = class _Granular {
15066
15193
  const environmentSession = new EnvironmentSession(
15067
15194
  client,
15068
15195
  environment,
15069
- clientId
15196
+ clientId,
15197
+ { initialQuota: session.quota || null }
15070
15198
  );
15071
15199
  await environmentSession.hello();
15072
15200
  return environmentSession;
@@ -17275,12 +17403,107 @@ ${knownFactsBlock}
17275
17403
  ${input.request?.trim() || "Use the latest user message in the conversation."}`;
17276
17404
  }
17277
17405
 
17406
+ // src/openai-usage.ts
17407
+ var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
17408
+ var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
17409
+ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
17410
+ "gpt-5.4": {
17411
+ provider: "openai",
17412
+ model: "gpt-5.4",
17413
+ currency: "USD",
17414
+ inputUsdPerMillion: 2.5,
17415
+ cachedInputUsdPerMillion: 0.25,
17416
+ outputUsdPerMillion: 15,
17417
+ sourceUrl: OPENAI_PRICING_SOURCE_URL,
17418
+ effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
17419
+ }
17420
+ };
17421
+ function asRecord5(value) {
17422
+ return value && typeof value === "object" ? value : null;
17423
+ }
17424
+ function numberField(record, key) {
17425
+ const value = record?.[key];
17426
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17427
+ }
17428
+ function microsPerMillion(usdPerMillion) {
17429
+ return Math.round(usdPerMillion * 1e6);
17430
+ }
17431
+ function getOpenAIModelPricing(model) {
17432
+ return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
17433
+ }
17434
+ function normalizeOpenAIUsage(rawUsage) {
17435
+ const usage = asRecord5(rawUsage);
17436
+ if (!usage) {
17437
+ return {
17438
+ inputTokens: 0,
17439
+ cachedInputTokens: 0,
17440
+ uncachedInputTokens: 0,
17441
+ outputTokens: 0,
17442
+ reasoningTokens: 0,
17443
+ totalTokens: 0
17444
+ };
17445
+ }
17446
+ const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17447
+ const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17448
+ const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17449
+ const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17450
+ const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
17451
+ const cachedInputTokens = Math.min(
17452
+ inputTokens,
17453
+ numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
17454
+ );
17455
+ const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
17456
+ return {
17457
+ inputTokens,
17458
+ cachedInputTokens,
17459
+ uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
17460
+ outputTokens,
17461
+ reasoningTokens,
17462
+ totalTokens
17463
+ };
17464
+ }
17465
+ function calculateOpenAITokenSpend(model, rawUsage) {
17466
+ const pricing = getOpenAIModelPricing(model);
17467
+ if (!pricing) return null;
17468
+ const usage = normalizeOpenAIUsage(rawUsage);
17469
+ const inputPricePerMillionMicros = microsPerMillion(
17470
+ pricing.inputUsdPerMillion
17471
+ );
17472
+ const cachedInputPricePerMillionMicros = microsPerMillion(
17473
+ pricing.cachedInputUsdPerMillion
17474
+ );
17475
+ const outputPricePerMillionMicros = microsPerMillion(
17476
+ pricing.outputUsdPerMillion
17477
+ );
17478
+ const amountMicros = Math.round(
17479
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
17480
+ );
17481
+ return {
17482
+ provider: "openai",
17483
+ model,
17484
+ inputTokens: usage.inputTokens,
17485
+ cachedInputTokens: usage.cachedInputTokens,
17486
+ uncachedInputTokens: usage.uncachedInputTokens,
17487
+ outputTokens: usage.outputTokens,
17488
+ reasoningTokens: usage.reasoningTokens,
17489
+ totalTokens: usage.totalTokens,
17490
+ amountMicros,
17491
+ currency: "USD",
17492
+ inputPricePerMillionMicros,
17493
+ cachedInputPricePerMillionMicros,
17494
+ outputPricePerMillionMicros,
17495
+ pricingSource: pricing.sourceUrl,
17496
+ pricingEffectiveAt: pricing.effectiveDate,
17497
+ usage
17498
+ };
17499
+ }
17500
+
17278
17501
  // src/agent-evals.ts
17279
17502
  var DEFAULT_CONTROLLER_BUDGETS = {
17280
17503
  maxIterations: 6,
17281
17504
  maxNoProgressIterations: 2
17282
17505
  };
17283
- function asRecord5(value) {
17506
+ function asRecord6(value) {
17284
17507
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
17285
17508
  return value;
17286
17509
  }
@@ -17333,9 +17556,9 @@ function asArray3(value) {
17333
17556
  return Array.isArray(value) ? value : [value];
17334
17557
  }
17335
17558
  var GPT_54_TOKEN_PRICING_USD_PER_MILLION = {
17336
- input: 0.75,
17337
- cachedInput: 0.075,
17338
- output: 4.5
17559
+ input: 2.5,
17560
+ cachedInput: 0.25,
17561
+ output: 15
17339
17562
  };
17340
17563
  function emptyTokenUsage() {
17341
17564
  return {
@@ -17352,14 +17575,24 @@ function emptyTokenUsage() {
17352
17575
  missingUsageCalls: 0
17353
17576
  };
17354
17577
  }
17355
- function numberField(record, key) {
17578
+ function numberField2(record, key) {
17356
17579
  const value = record?.[key];
17357
17580
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
17358
17581
  }
17359
17582
  function calculateTokenCost(input) {
17360
- const inputCostUsd = input.uncachedInputTokens * GPT_54_TOKEN_PRICING_USD_PER_MILLION.input / 1e6;
17361
- const cachedInputCostUsd = input.cachedInputTokens * GPT_54_TOKEN_PRICING_USD_PER_MILLION.cachedInput / 1e6;
17362
- const outputCostUsd = input.outputTokens * GPT_54_TOKEN_PRICING_USD_PER_MILLION.output / 1e6;
17583
+ const spend = input.model ? calculateOpenAITokenSpend(input.model, {
17584
+ input_tokens: input.uncachedInputTokens + input.cachedInputTokens,
17585
+ output_tokens: input.outputTokens,
17586
+ input_tokens_details: { cached_tokens: input.cachedInputTokens }
17587
+ }) : null;
17588
+ const pricing = spend ? {
17589
+ input: spend.inputPricePerMillionMicros / 1e6,
17590
+ cachedInput: spend.cachedInputPricePerMillionMicros / 1e6,
17591
+ output: spend.outputPricePerMillionMicros / 1e6
17592
+ } : GPT_54_TOKEN_PRICING_USD_PER_MILLION;
17593
+ const inputCostUsd = input.uncachedInputTokens * pricing.input / 1e6;
17594
+ const cachedInputCostUsd = input.cachedInputTokens * pricing.cachedInput / 1e6;
17595
+ const outputCostUsd = input.outputTokens * pricing.output / 1e6;
17363
17596
  return {
17364
17597
  inputCostUsd,
17365
17598
  cachedInputCostUsd,
@@ -17368,18 +17601,20 @@ function calculateTokenCost(input) {
17368
17601
  };
17369
17602
  }
17370
17603
  function extractTokenUsageFromRaw(raw) {
17371
- const usage = asRecord5(asRecord5(raw)?.usage);
17604
+ const usage = asRecord6(asRecord6(raw)?.usage);
17372
17605
  if (!usage) return null;
17373
- const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17374
- const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17375
- const details = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17606
+ const model = typeof asRecord6(raw)?.model === "string" ? asRecord6(raw)?.model : void 0;
17607
+ const inputTokens = numberField2(usage, "prompt_tokens") || numberField2(usage, "input_tokens");
17608
+ const outputTokens = numberField2(usage, "completion_tokens") || numberField2(usage, "output_tokens");
17609
+ const details = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
17376
17610
  const cachedInputTokens = Math.min(
17377
17611
  inputTokens,
17378
- numberField(details, "cached_tokens") || numberField(details, "cached_input_tokens")
17612
+ numberField2(details, "cached_tokens") || numberField2(details, "cached_input_tokens")
17379
17613
  );
17380
17614
  const uncachedInputTokens = Math.max(inputTokens - cachedInputTokens, 0);
17381
- const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17615
+ const totalTokens = numberField2(usage, "total_tokens") || inputTokens + outputTokens;
17382
17616
  const costs = calculateTokenCost({
17617
+ model,
17383
17618
  uncachedInputTokens,
17384
17619
  cachedInputTokens,
17385
17620
  outputTokens
@@ -17430,7 +17665,7 @@ function aggregateTokenUsage(usages) {
17430
17665
  }
17431
17666
  function aggregateConversationTokenUsage(conversation) {
17432
17667
  return aggregateTokenUsage(
17433
- conversation.logTurns.flatMap(
17668
+ (conversation.logTurns || []).flatMap(
17434
17669
  (turn) => turn.iterations.map((iteration) => iteration.tokenUsage)
17435
17670
  )
17436
17671
  );
@@ -17464,8 +17699,8 @@ function formatTokenUsage(usage) {
17464
17699
  ];
17465
17700
  }
17466
17701
  function getJobAgentMessages(liveDoc, jobId) {
17467
- const jobsById = asRecord5(asRecord5(liveDoc.jobs)?.byId);
17468
- const job = asRecord5(jobsById?.[jobId]);
17702
+ const jobsById = asRecord6(asRecord6(liveDoc.jobs)?.byId);
17703
+ const job = asRecord6(jobsById?.[jobId]);
17469
17704
  const agentMessages = job?.agentMessages;
17470
17705
  return Array.isArray(agentMessages) ? agentMessages : [];
17471
17706
  }
@@ -17524,12 +17759,12 @@ function buildHistory(entries) {
17524
17759
  );
17525
17760
  }
17526
17761
  function getOpenPromptsFromDoc(liveDoc) {
17527
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17762
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
17528
17763
  const prompts = [];
17529
17764
  for (const job of Object.values(jobsById)) {
17530
- const promptRecords = asRecord5(asRecord5(job)?.prompts) || {};
17765
+ const promptRecords = asRecord6(asRecord6(job)?.prompts) || {};
17531
17766
  for (const raw of Object.values(promptRecords)) {
17532
- const record = asRecord5(raw);
17767
+ const record = asRecord6(raw);
17533
17768
  if (!record || record.status !== "open" || typeof record.promptId !== "string")
17534
17769
  continue;
17535
17770
  const prompt = normalizePrompt({
@@ -17550,11 +17785,11 @@ function getOpenPromptsFromDoc(liveDoc) {
17550
17785
  return prompts;
17551
17786
  }
17552
17787
  function filterPromptsByBoundary(liveDoc, prompts, boundaryTimestamp) {
17553
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17788
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
17554
17789
  return prompts.filter((prompt) => {
17555
17790
  for (const jobRecord of Object.values(jobsById)) {
17556
- const promptsById = asRecord5(asRecord5(jobRecord)?.prompts) || {};
17557
- const promptRecord = asRecord5(promptsById[prompt.id]);
17791
+ const promptsById = asRecord6(asRecord6(jobRecord)?.prompts) || {};
17792
+ const promptRecord = asRecord6(promptsById[prompt.id]);
17558
17793
  const openedAt = Number(promptRecord?.openedAt) || 0;
17559
17794
  if (openedAt >= boundaryTimestamp) return true;
17560
17795
  }
@@ -17686,6 +17921,29 @@ function createOpenAIChatTurnGenerator(options) {
17686
17921
  ""
17687
17922
  );
17688
17923
  const model = options.model || "gpt-5.4";
17924
+ const client = new OpenAI__default.default({
17925
+ apiKey: options.apiKey,
17926
+ baseURL: baseUrl,
17927
+ defaultHeaders: options.headers
17928
+ });
17929
+ const emitUsage = async (rawUsage, requestId, usageContext) => {
17930
+ if (!rawUsage || !options.onUsage) return;
17931
+ const spend = calculateOpenAITokenSpend(model, rawUsage);
17932
+ if (!spend) return;
17933
+ const mergedUsageContext = {
17934
+ ...options.usageContext || {},
17935
+ ...usageContext || {}
17936
+ };
17937
+ await options.onUsage({
17938
+ ...spend,
17939
+ source: "openai",
17940
+ lineItemType: "llm_tokens",
17941
+ operation: "chat.completions",
17942
+ requestId: requestId || null,
17943
+ usageContext: mergedUsageContext,
17944
+ rawUsage
17945
+ });
17946
+ };
17689
17947
  return async (input) => {
17690
17948
  const messages = [
17691
17949
  {
@@ -17702,80 +17960,46 @@ ${modelOutputInstruction()}`
17702
17960
  messages,
17703
17961
  response_format: { type: "json_object" }
17704
17962
  };
17705
- if (input.onTextDelta) {
17706
- payload.stream = true;
17707
- }
17708
17963
  if (typeof options.temperature === "number") {
17709
17964
  payload.temperature = options.temperature;
17710
17965
  }
17711
17966
  let lastError = null;
17712
17967
  for (let attempt = 1; attempt <= 3; attempt += 1) {
17713
17968
  try {
17714
- const response = await fetch(`${baseUrl}/chat/completions`, {
17715
- method: "POST",
17716
- headers: {
17717
- "content-type": "application/json",
17718
- authorization: `Bearer ${options.apiKey}`,
17719
- ...options.headers
17720
- },
17721
- body: JSON.stringify(payload)
17722
- });
17723
- if (!response.ok) {
17724
- const errorText = await response.text();
17725
- if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
17726
- await sleep2(500 * attempt);
17727
- continue;
17728
- }
17729
- throw new Error(
17730
- `OpenAI chat generation failed: ${response.status} ${errorText}`
17731
- );
17732
- }
17733
17969
  let raw;
17734
17970
  let text = "";
17735
- if (input.onTextDelta && response.body) {
17971
+ let usage = null;
17972
+ let requestId = null;
17973
+ if (input.onTextDelta) {
17736
17974
  const onTextDelta = input.onTextDelta;
17737
- const reader = response.body.getReader();
17738
- const decoder = new TextDecoder();
17739
- let buffer = "";
17740
- const processStreamLine = async (line) => {
17741
- const trimmedLine = line.trimEnd();
17742
- if (!trimmedLine.startsWith("data:")) return;
17743
- const data = trimmedLine.slice("data:".length).trim();
17744
- if (!data || data === "[DONE]") return;
17745
- const event = JSON.parse(data);
17746
- const delta = asRecord5(
17747
- asRecord5(event.choices?.[0])?.delta
17748
- )?.content;
17749
- const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord5(part)?.text || "").join("") : "";
17750
- if (!deltaText) return;
17975
+ const stream = await client.chat.completions.create({
17976
+ ...payload,
17977
+ stream: true,
17978
+ stream_options: { include_usage: true }
17979
+ });
17980
+ for await (const event of stream) {
17981
+ requestId = requestId || event.id || event._request_id || null;
17982
+ usage = event.usage || usage;
17983
+ const delta = event.choices?.[0]?.delta?.content;
17984
+ const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord6(part)?.text || "").join("") : "";
17985
+ if (!deltaText) continue;
17751
17986
  text += deltaText;
17752
17987
  await onTextDelta(deltaText);
17753
- };
17754
- while (true) {
17755
- const { value, done } = await reader.read();
17756
- if (done) break;
17757
- buffer += decoder.decode(value, { stream: true });
17758
- while (true) {
17759
- const lineEnd = buffer.indexOf("\n");
17760
- if (lineEnd === -1) break;
17761
- const line = buffer.slice(0, lineEnd);
17762
- buffer = buffer.slice(lineEnd + 1);
17763
- await processStreamLine(line);
17764
- }
17765
17988
  }
17766
- buffer += decoder.decode();
17767
- if (buffer.trim()) {
17768
- await processStreamLine(buffer);
17769
- }
17770
- raw = { streamed: true };
17989
+ raw = { streamed: true, model, usage, request_id: requestId };
17771
17990
  } else {
17772
- const json = await response.json();
17773
- raw = json;
17774
- const content = asRecord5(
17775
- asRecord5(json.choices?.[0])?.message
17991
+ const completion = await client.chat.completions.create(
17992
+ payload
17993
+ );
17994
+ raw = completion;
17995
+ usage = completion.usage;
17996
+ requestId = (typeof completion.id === "string" ? completion.id : null) || (typeof completion._request_id === "string" ? completion._request_id : null);
17997
+ const content = asRecord6(
17998
+ asRecord6(completion.choices?.[0])?.message
17776
17999
  )?.content;
17777
- text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord5(part)?.text || "").join("") : "";
18000
+ text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord6(part)?.text || "").join("") : "";
17778
18001
  }
18002
+ await emitUsage(usage, requestId, input.usageContext);
17779
18003
  const parsed = extractJsonObject(text);
17780
18004
  if (!parsed) {
17781
18005
  if (attempt < 3) {
@@ -17795,9 +18019,10 @@ ${modelOutputInstruction()}`
17795
18019
  };
17796
18020
  } catch (error) {
17797
18021
  lastError = error instanceof Error ? error : new Error(String(error));
17798
- if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
18022
+ const status = Number(error?.status);
18023
+ if (attempt < 3 && (Number.isFinite(status) && (status >= 500 || status === 429) || /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
17799
18024
  lastError.message
17800
- )) {
18025
+ ))) {
17801
18026
  await sleep2(500 * attempt);
17802
18027
  continue;
17803
18028
  }
@@ -17830,12 +18055,12 @@ async function withTimeout2(promise, ms, label) {
17830
18055
  }
17831
18056
  }
17832
18057
  function getActionSummary(liveDoc, jobId) {
17833
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17834
- const job = asRecord5(jobsById[jobId]);
18058
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
18059
+ const job = asRecord6(jobsById[jobId]);
17835
18060
  const summary = Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
17836
18061
  (line) => typeof line === "string"
17837
18062
  ) : [];
17838
- const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord5(event)).filter((event) => Boolean(event)) : [];
18063
+ const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord6(event)).filter((event) => Boolean(event)) : [];
17839
18064
  const traceLines = trace.map((event) => {
17840
18065
  const kind = typeof event.kind === "string" ? event.kind : "";
17841
18066
  const action = typeof event.action === "string" ? event.action : "";
@@ -17856,9 +18081,9 @@ function getActionSummary(liveDoc, jobId) {
17856
18081
  }
17857
18082
  function normalizeHeapSnapshot2(heap) {
17858
18083
  return {
17859
- entriesByPath: asRecord5(heap?.entriesByPath) || {},
17860
- listsByName: asRecord5(heap?.listsByName) || {},
17861
- variablesByName: asRecord5(heap?.variablesByName) || {},
18084
+ entriesByPath: asRecord6(heap?.entriesByPath) || {},
18085
+ listsByName: asRecord6(heap?.listsByName) || {},
18086
+ variablesByName: asRecord6(heap?.variablesByName) || {},
17862
18087
  updatedAt: typeof heap?.updatedAt === "number" ? heap.updatedAt : Date.now()
17863
18088
  };
17864
18089
  }
@@ -17893,9 +18118,9 @@ async function waitForJobOutcome(input) {
17893
18118
  input.boundaryTimestamp
17894
18119
  );
17895
18120
  lastPromptCount = prompts.length;
17896
- const messages = asArray3(asRecord5(liveDoc.conversation)?.messages);
18121
+ const messages = asArray3(asRecord6(liveDoc.conversation)?.messages);
17897
18122
  lastMessageCount = messages.length;
17898
- lastJobSummary = asRecord5(asRecord5(liveDoc.jobs)?.byId)?.[input.job.id] || null;
18123
+ lastJobSummary = asRecord6(asRecord6(liveDoc.jobs)?.byId)?.[input.job.id] || null;
17899
18124
  if (prompts.length > 0) {
17900
18125
  return { kind: "prompt", prompts, liveDoc, stdout, stderr };
17901
18126
  }
@@ -18079,7 +18304,8 @@ function jsonBlock(value) {
18079
18304
  }
18080
18305
  function buildSessionLogReport(input) {
18081
18306
  const { conversation, result, error } = input;
18082
- const systemPrompts = conversation.logTurns.flatMap(
18307
+ const logTurns = conversation.logTurns || [];
18308
+ const systemPrompts = logTurns.flatMap(
18083
18309
  (turn) => turn.iterations.map((iteration) => ({
18084
18310
  turn,
18085
18311
  iteration
@@ -18108,7 +18334,7 @@ function buildSessionLogReport(input) {
18108
18334
  "",
18109
18335
  "## Conversation"
18110
18336
  ];
18111
- for (const turn of conversation.logTurns) {
18337
+ for (const turn of logTurns) {
18112
18338
  lines.push("", `### Turn ${turn.turnNumber}: ${turn.turnId}`, "");
18113
18339
  lines.push("**User**", "");
18114
18340
  lines.push(turn.request, "");
@@ -18306,7 +18532,7 @@ async function runAgentEvalSuite(options) {
18306
18532
  promptInteractions: completed.promptInteractions,
18307
18533
  result: completed.result,
18308
18534
  heap: normalizeHeapSnapshot2(
18309
- asRecord5(
18535
+ asRecord6(
18310
18536
  cloneJson(conversation.environment.document)?.heap
18311
18537
  )
18312
18538
  ),
@@ -18416,7 +18642,7 @@ async function runAgentEvalSuite(options) {
18416
18642
  finalResult = result;
18417
18643
  } catch (error) {
18418
18644
  const failureMessage = error instanceof Error ? error.message : String(error);
18419
- const failedTurn = conversation.logTurns[conversation.logTurns.length - 1];
18645
+ const failedTurn = conversation.logTurns?.[conversation.logTurns.length - 1];
18420
18646
  if (failedTurn && !failedTurn.completed) {
18421
18647
  failedTurn.error = failureMessage;
18422
18648
  const failedIteration = latestIterationLog(failedTurn);
@@ -18549,7 +18775,7 @@ function createAgentEvalHarness(options) {
18549
18775
  }
18550
18776
  function buildCheckContext(conversation, completed, turnDir) {
18551
18777
  const liveDoc = cloneJson(conversation.environment.document);
18552
- const heap = normalizeHeapSnapshot2(asRecord5(liveDoc?.heap));
18778
+ const heap = normalizeHeapSnapshot2(asRecord6(liveDoc?.heap));
18553
18779
  return {
18554
18780
  conversation,
18555
18781
  environment: conversation.environment,
@@ -18643,7 +18869,7 @@ function createAgentEvalHarness(options) {
18643
18869
  result: resumed.result,
18644
18870
  stdout: [...pending.stdout, ...resumed.stdout],
18645
18871
  agentMessages: getJobAgentMessages(liveDoc, pending.job.id),
18646
- sessionHeap: normalizeHeapSnapshot2(asRecord5(liveDoc?.heap))
18872
+ sessionHeap: normalizeHeapSnapshot2(asRecord6(liveDoc?.heap))
18647
18873
  });
18648
18874
  const responseText = presentation.responseText || pending.finalReply || "Done.";
18649
18875
  pending.conversation.history.push({
@@ -18767,7 +18993,7 @@ function createAgentEvalHarness(options) {
18767
18993
  environmentId: conversation.environment.environmentId,
18768
18994
  domainRevision: conversation.environment.domainRevision
18769
18995
  },
18770
- heapSummary: projectHeapSummary(asRecord5(liveDoc?.heap), {
18996
+ heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
18771
18997
  focus: heapFocus
18772
18998
  }),
18773
18999
  referentSummary: projectConversationReferentSummary(liveDoc),
@@ -18789,7 +19015,14 @@ function createAgentEvalHarness(options) {
18789
19015
  history: buildHistory(conversation.history),
18790
19016
  request,
18791
19017
  attempt: 1,
18792
- tools
19018
+ tools,
19019
+ usageContext: {
19020
+ sandboxId: conversation.environment.sandboxId,
19021
+ environmentId: conversation.environment.environmentId,
19022
+ sessionId: conversation.environment.sessionId,
19023
+ subjectId: conversation.environment.subjectId,
19024
+ permissionProfileId: conversation.environment.permissionProfileId
19025
+ }
18793
19026
  }),
18794
19027
  chatTimeoutMs,
18795
19028
  `chat generation for ${conversation.label} iteration ${iteration + 1}`
@@ -18938,7 +19171,7 @@ function createAgentEvalHarness(options) {
18938
19171
  const settledLiveDoc = cloneJson(
18939
19172
  conversation.environment.document
18940
19173
  );
18941
- const sessionHeap = normalizeHeapSnapshot2(asRecord5(settledLiveDoc?.heap));
19174
+ const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
18942
19175
  const presentation = resolveJobPresentation({
18943
19176
  jobId: job.id,
18944
19177
  result: outcome.result,
@@ -19062,7 +19295,10 @@ function createAgentTester(options) {
19062
19295
  model: options.openai?.model || options.model,
19063
19296
  baseUrl: options.openai?.baseUrl,
19064
19297
  temperature: options.openai?.temperature,
19065
- headers: options.openai?.headers
19298
+ headers: options.openai?.headers,
19299
+ onUsage: async (usage) => {
19300
+ await granular.recordOpenAIUsageSpend(usage, usage.usageContext);
19301
+ }
19066
19302
  });
19067
19303
  let resolvedEnvironmentId = "environmentId" in options.target ? options.target.environmentId : null;
19068
19304
  let connectSeeded = false;