@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.
@@ -1,5 +1,6 @@
1
1
  import { writeFile, mkdir } from 'fs/promises';
2
2
  import path from 'path';
3
+ import OpenAI from 'openai';
3
4
  import * as Automerge from '@automerge/automerge';
4
5
 
5
6
  var __create = Object.create;
@@ -4618,6 +4619,7 @@ function withPromptTranscriptTimeout(promise) {
4618
4619
  var Session = class {
4619
4620
  client;
4620
4621
  clientId;
4622
+ initialQuota;
4621
4623
  jobsMap = /* @__PURE__ */ new Map();
4622
4624
  pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
4623
4625
  eventListeners = /* @__PURE__ */ new Map();
@@ -4633,9 +4635,10 @@ var Session = class {
4633
4635
  promptCache = /* @__PURE__ */ new Map();
4634
4636
  /** Prompt ids locally answered before the document sync catches up. */
4635
4637
  hiddenPromptIds = /* @__PURE__ */ new Set();
4636
- constructor(client, clientId) {
4638
+ constructor(client, clientId, options = {}) {
4637
4639
  this.client = client;
4638
4640
  this.clientId = clientId || `client_${Date.now()}`;
4641
+ this.initialQuota = options.initialQuota || null;
4639
4642
  this.setupEventHandlers();
4640
4643
  this.setupToolInvokeHandler();
4641
4644
  }
@@ -4684,6 +4687,16 @@ var Session = class {
4684
4687
  get document() {
4685
4688
  return this.client.doc;
4686
4689
  }
4690
+ get quota() {
4691
+ return this.getQuota();
4692
+ }
4693
+ getQuota() {
4694
+ const quota = this.client.doc.billing?.quota;
4695
+ if (quota && typeof quota === "object") {
4696
+ return quota;
4697
+ }
4698
+ return this.initialQuota;
4699
+ }
4687
4700
  get sessionId() {
4688
4701
  return this.client.currentSessionId;
4689
4702
  }
@@ -11306,6 +11319,110 @@ async function invokeRegisteredEffect(effectMap, request) {
11306
11319
  return resolved.handler(request.input, context);
11307
11320
  }
11308
11321
 
11322
+ // src/spend.ts
11323
+ function toGranularHttpBase(apiUrl) {
11324
+ const url = new URL(apiUrl);
11325
+ if (url.protocol === "ws:") {
11326
+ url.protocol = "http:";
11327
+ } else if (url.protocol === "wss:") {
11328
+ url.protocol = "https:";
11329
+ }
11330
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
11331
+ if (!url.pathname || url.pathname === "/") {
11332
+ url.pathname = "/granular";
11333
+ }
11334
+ url.search = "";
11335
+ url.hash = "";
11336
+ return url.toString().replace(/\/$/, "");
11337
+ }
11338
+ function cleanIdPart(value) {
11339
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
11340
+ }
11341
+ function buildOpenAISpendEventId(usage, context = {}) {
11342
+ const requestId = usage.requestId?.trim();
11343
+ if (!requestId) return void 0;
11344
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
11345
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
11346
+ }
11347
+ function pricingEffectiveAtSeconds(value) {
11348
+ if (!value) return null;
11349
+ const parsed = Date.parse(value);
11350
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
11351
+ }
11352
+ function compactContext(context) {
11353
+ return Object.fromEntries(
11354
+ Object.entries(context).filter(
11355
+ ([, value]) => value != null && value !== ""
11356
+ )
11357
+ );
11358
+ }
11359
+ function omitTenantId(context) {
11360
+ const scopedContext = { ...context };
11361
+ delete scopedContext.tenantId;
11362
+ return scopedContext;
11363
+ }
11364
+ async function recordOpenAIUsageSpend(options) {
11365
+ const usageContext = compactContext({
11366
+ ...options.usage.usageContext || {},
11367
+ ...options.context || {}
11368
+ });
11369
+ const context = omitTenantId(usageContext);
11370
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
11371
+ const metadata = {
11372
+ ...options.metadata || {},
11373
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
11374
+ usageContext: context
11375
+ };
11376
+ const response = await fetch(
11377
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
11378
+ {
11379
+ method: "POST",
11380
+ cache: "no-store",
11381
+ headers: {
11382
+ Authorization: `Bearer ${options.token}`,
11383
+ "Content-Type": "application/json"
11384
+ },
11385
+ body: JSON.stringify({
11386
+ ...spendEventId ? { spendEventId } : {},
11387
+ sandboxId: context.sandboxId || null,
11388
+ environmentId: context.environmentId || null,
11389
+ sessionId: context.sessionId || null,
11390
+ subjectId: context.subjectId || null,
11391
+ permissionProfileId: context.permissionProfileId || null,
11392
+ source: "openai",
11393
+ lineItemType: "llm_tokens",
11394
+ provider: options.usage.provider,
11395
+ model: options.usage.model,
11396
+ operation: options.usage.operation || "chat.completions",
11397
+ requestId: options.usage.requestId || null,
11398
+ inputTokens: options.usage.inputTokens,
11399
+ outputTokens: options.usage.outputTokens,
11400
+ cachedInputTokens: options.usage.cachedInputTokens,
11401
+ reasoningTokens: options.usage.reasoningTokens,
11402
+ quantity: options.usage.totalTokens,
11403
+ quantityUnit: "tokens",
11404
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
11405
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
11406
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
11407
+ amountMicros: options.usage.amountMicros,
11408
+ currency: options.usage.currency,
11409
+ pricingSource: options.usage.pricingSource,
11410
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
11411
+ options.usage.pricingEffectiveAt
11412
+ ),
11413
+ estimated: false,
11414
+ metadata
11415
+ })
11416
+ }
11417
+ );
11418
+ if (!response.ok) {
11419
+ throw new Error(
11420
+ `Granular spend event failed (${response.status}): ${await response.text()}`
11421
+ );
11422
+ }
11423
+ return response.json();
11424
+ }
11425
+
11309
11426
  // ../metamodel-enum/src/index.ts
11310
11427
  function renderInlineStringUnion(values) {
11311
11428
  return values.map((value) => JSON.stringify(value)).join(" | ");
@@ -14088,7 +14205,7 @@ var EnvironmentSession = class extends Session {
14088
14205
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
14089
14206
  graphContainerStatus = null;
14090
14207
  constructor(client, environment, clientId, options = {}) {
14091
- super(client, clientId);
14208
+ super(client, clientId, { initialQuota: options.initialQuota });
14092
14209
  this.environment = environment;
14093
14210
  this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
14094
14211
  }
@@ -14887,6 +15004,15 @@ var Granular = class _Granular {
14887
15004
  const environment = this.bindEnvironmentHandle(envData);
14888
15005
  return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
14889
15006
  }
15007
+ async recordOpenAIUsageSpend(usage, context, options) {
15008
+ return recordOpenAIUsageSpend({
15009
+ apiUrl: this.apiUrl,
15010
+ token: this.apiKey,
15011
+ usage,
15012
+ context,
15013
+ metadata: options?.metadata
15014
+ });
15015
+ }
14890
15016
  /**
14891
15017
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
14892
15018
  * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
@@ -15041,7 +15167,8 @@ var Granular = class _Granular {
15041
15167
  const environmentSession = new EnvironmentSession(
15042
15168
  client,
15043
15169
  environment,
15044
- clientId
15170
+ clientId,
15171
+ { initialQuota: session.quota || null }
15045
15172
  );
15046
15173
  await environmentSession.hello();
15047
15174
  return environmentSession;
@@ -17250,12 +17377,107 @@ ${knownFactsBlock}
17250
17377
  ${input.request?.trim() || "Use the latest user message in the conversation."}`;
17251
17378
  }
17252
17379
 
17380
+ // src/openai-usage.ts
17381
+ var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
17382
+ var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
17383
+ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
17384
+ "gpt-5.4": {
17385
+ provider: "openai",
17386
+ model: "gpt-5.4",
17387
+ currency: "USD",
17388
+ inputUsdPerMillion: 2.5,
17389
+ cachedInputUsdPerMillion: 0.25,
17390
+ outputUsdPerMillion: 15,
17391
+ sourceUrl: OPENAI_PRICING_SOURCE_URL,
17392
+ effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
17393
+ }
17394
+ };
17395
+ function asRecord5(value) {
17396
+ return value && typeof value === "object" ? value : null;
17397
+ }
17398
+ function numberField(record, key) {
17399
+ const value = record?.[key];
17400
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17401
+ }
17402
+ function microsPerMillion(usdPerMillion) {
17403
+ return Math.round(usdPerMillion * 1e6);
17404
+ }
17405
+ function getOpenAIModelPricing(model) {
17406
+ return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
17407
+ }
17408
+ function normalizeOpenAIUsage(rawUsage) {
17409
+ const usage = asRecord5(rawUsage);
17410
+ if (!usage) {
17411
+ return {
17412
+ inputTokens: 0,
17413
+ cachedInputTokens: 0,
17414
+ uncachedInputTokens: 0,
17415
+ outputTokens: 0,
17416
+ reasoningTokens: 0,
17417
+ totalTokens: 0
17418
+ };
17419
+ }
17420
+ const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17421
+ const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17422
+ const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17423
+ const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17424
+ const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
17425
+ const cachedInputTokens = Math.min(
17426
+ inputTokens,
17427
+ numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
17428
+ );
17429
+ const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
17430
+ return {
17431
+ inputTokens,
17432
+ cachedInputTokens,
17433
+ uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
17434
+ outputTokens,
17435
+ reasoningTokens,
17436
+ totalTokens
17437
+ };
17438
+ }
17439
+ function calculateOpenAITokenSpend(model, rawUsage) {
17440
+ const pricing = getOpenAIModelPricing(model);
17441
+ if (!pricing) return null;
17442
+ const usage = normalizeOpenAIUsage(rawUsage);
17443
+ const inputPricePerMillionMicros = microsPerMillion(
17444
+ pricing.inputUsdPerMillion
17445
+ );
17446
+ const cachedInputPricePerMillionMicros = microsPerMillion(
17447
+ pricing.cachedInputUsdPerMillion
17448
+ );
17449
+ const outputPricePerMillionMicros = microsPerMillion(
17450
+ pricing.outputUsdPerMillion
17451
+ );
17452
+ const amountMicros = Math.round(
17453
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
17454
+ );
17455
+ return {
17456
+ provider: "openai",
17457
+ model,
17458
+ inputTokens: usage.inputTokens,
17459
+ cachedInputTokens: usage.cachedInputTokens,
17460
+ uncachedInputTokens: usage.uncachedInputTokens,
17461
+ outputTokens: usage.outputTokens,
17462
+ reasoningTokens: usage.reasoningTokens,
17463
+ totalTokens: usage.totalTokens,
17464
+ amountMicros,
17465
+ currency: "USD",
17466
+ inputPricePerMillionMicros,
17467
+ cachedInputPricePerMillionMicros,
17468
+ outputPricePerMillionMicros,
17469
+ pricingSource: pricing.sourceUrl,
17470
+ pricingEffectiveAt: pricing.effectiveDate,
17471
+ usage
17472
+ };
17473
+ }
17474
+
17253
17475
  // src/agent-evals.ts
17254
17476
  var DEFAULT_CONTROLLER_BUDGETS = {
17255
17477
  maxIterations: 6,
17256
17478
  maxNoProgressIterations: 2
17257
17479
  };
17258
- function asRecord5(value) {
17480
+ function asRecord6(value) {
17259
17481
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
17260
17482
  return value;
17261
17483
  }
@@ -17308,9 +17530,9 @@ function asArray3(value) {
17308
17530
  return Array.isArray(value) ? value : [value];
17309
17531
  }
17310
17532
  var GPT_54_TOKEN_PRICING_USD_PER_MILLION = {
17311
- input: 0.75,
17312
- cachedInput: 0.075,
17313
- output: 4.5
17533
+ input: 2.5,
17534
+ cachedInput: 0.25,
17535
+ output: 15
17314
17536
  };
17315
17537
  function emptyTokenUsage() {
17316
17538
  return {
@@ -17327,14 +17549,24 @@ function emptyTokenUsage() {
17327
17549
  missingUsageCalls: 0
17328
17550
  };
17329
17551
  }
17330
- function numberField(record, key) {
17552
+ function numberField2(record, key) {
17331
17553
  const value = record?.[key];
17332
17554
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
17333
17555
  }
17334
17556
  function calculateTokenCost(input) {
17335
- const inputCostUsd = input.uncachedInputTokens * GPT_54_TOKEN_PRICING_USD_PER_MILLION.input / 1e6;
17336
- const cachedInputCostUsd = input.cachedInputTokens * GPT_54_TOKEN_PRICING_USD_PER_MILLION.cachedInput / 1e6;
17337
- const outputCostUsd = input.outputTokens * GPT_54_TOKEN_PRICING_USD_PER_MILLION.output / 1e6;
17557
+ const spend = input.model ? calculateOpenAITokenSpend(input.model, {
17558
+ input_tokens: input.uncachedInputTokens + input.cachedInputTokens,
17559
+ output_tokens: input.outputTokens,
17560
+ input_tokens_details: { cached_tokens: input.cachedInputTokens }
17561
+ }) : null;
17562
+ const pricing = spend ? {
17563
+ input: spend.inputPricePerMillionMicros / 1e6,
17564
+ cachedInput: spend.cachedInputPricePerMillionMicros / 1e6,
17565
+ output: spend.outputPricePerMillionMicros / 1e6
17566
+ } : GPT_54_TOKEN_PRICING_USD_PER_MILLION;
17567
+ const inputCostUsd = input.uncachedInputTokens * pricing.input / 1e6;
17568
+ const cachedInputCostUsd = input.cachedInputTokens * pricing.cachedInput / 1e6;
17569
+ const outputCostUsd = input.outputTokens * pricing.output / 1e6;
17338
17570
  return {
17339
17571
  inputCostUsd,
17340
17572
  cachedInputCostUsd,
@@ -17343,18 +17575,20 @@ function calculateTokenCost(input) {
17343
17575
  };
17344
17576
  }
17345
17577
  function extractTokenUsageFromRaw(raw) {
17346
- const usage = asRecord5(asRecord5(raw)?.usage);
17578
+ const usage = asRecord6(asRecord6(raw)?.usage);
17347
17579
  if (!usage) return null;
17348
- const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17349
- const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17350
- const details = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17580
+ const model = typeof asRecord6(raw)?.model === "string" ? asRecord6(raw)?.model : void 0;
17581
+ const inputTokens = numberField2(usage, "prompt_tokens") || numberField2(usage, "input_tokens");
17582
+ const outputTokens = numberField2(usage, "completion_tokens") || numberField2(usage, "output_tokens");
17583
+ const details = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
17351
17584
  const cachedInputTokens = Math.min(
17352
17585
  inputTokens,
17353
- numberField(details, "cached_tokens") || numberField(details, "cached_input_tokens")
17586
+ numberField2(details, "cached_tokens") || numberField2(details, "cached_input_tokens")
17354
17587
  );
17355
17588
  const uncachedInputTokens = Math.max(inputTokens - cachedInputTokens, 0);
17356
- const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17589
+ const totalTokens = numberField2(usage, "total_tokens") || inputTokens + outputTokens;
17357
17590
  const costs = calculateTokenCost({
17591
+ model,
17358
17592
  uncachedInputTokens,
17359
17593
  cachedInputTokens,
17360
17594
  outputTokens
@@ -17405,7 +17639,7 @@ function aggregateTokenUsage(usages) {
17405
17639
  }
17406
17640
  function aggregateConversationTokenUsage(conversation) {
17407
17641
  return aggregateTokenUsage(
17408
- conversation.logTurns.flatMap(
17642
+ (conversation.logTurns || []).flatMap(
17409
17643
  (turn) => turn.iterations.map((iteration) => iteration.tokenUsage)
17410
17644
  )
17411
17645
  );
@@ -17439,8 +17673,8 @@ function formatTokenUsage(usage) {
17439
17673
  ];
17440
17674
  }
17441
17675
  function getJobAgentMessages(liveDoc, jobId) {
17442
- const jobsById = asRecord5(asRecord5(liveDoc.jobs)?.byId);
17443
- const job = asRecord5(jobsById?.[jobId]);
17676
+ const jobsById = asRecord6(asRecord6(liveDoc.jobs)?.byId);
17677
+ const job = asRecord6(jobsById?.[jobId]);
17444
17678
  const agentMessages = job?.agentMessages;
17445
17679
  return Array.isArray(agentMessages) ? agentMessages : [];
17446
17680
  }
@@ -17499,12 +17733,12 @@ function buildHistory(entries) {
17499
17733
  );
17500
17734
  }
17501
17735
  function getOpenPromptsFromDoc(liveDoc) {
17502
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17736
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
17503
17737
  const prompts = [];
17504
17738
  for (const job of Object.values(jobsById)) {
17505
- const promptRecords = asRecord5(asRecord5(job)?.prompts) || {};
17739
+ const promptRecords = asRecord6(asRecord6(job)?.prompts) || {};
17506
17740
  for (const raw of Object.values(promptRecords)) {
17507
- const record = asRecord5(raw);
17741
+ const record = asRecord6(raw);
17508
17742
  if (!record || record.status !== "open" || typeof record.promptId !== "string")
17509
17743
  continue;
17510
17744
  const prompt = normalizePrompt({
@@ -17525,11 +17759,11 @@ function getOpenPromptsFromDoc(liveDoc) {
17525
17759
  return prompts;
17526
17760
  }
17527
17761
  function filterPromptsByBoundary(liveDoc, prompts, boundaryTimestamp) {
17528
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17762
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
17529
17763
  return prompts.filter((prompt) => {
17530
17764
  for (const jobRecord of Object.values(jobsById)) {
17531
- const promptsById = asRecord5(asRecord5(jobRecord)?.prompts) || {};
17532
- const promptRecord = asRecord5(promptsById[prompt.id]);
17765
+ const promptsById = asRecord6(asRecord6(jobRecord)?.prompts) || {};
17766
+ const promptRecord = asRecord6(promptsById[prompt.id]);
17533
17767
  const openedAt = Number(promptRecord?.openedAt) || 0;
17534
17768
  if (openedAt >= boundaryTimestamp) return true;
17535
17769
  }
@@ -17661,6 +17895,29 @@ function createOpenAIChatTurnGenerator(options) {
17661
17895
  ""
17662
17896
  );
17663
17897
  const model = options.model || "gpt-5.4";
17898
+ const client = new OpenAI({
17899
+ apiKey: options.apiKey,
17900
+ baseURL: baseUrl,
17901
+ defaultHeaders: options.headers
17902
+ });
17903
+ const emitUsage = async (rawUsage, requestId, usageContext) => {
17904
+ if (!rawUsage || !options.onUsage) return;
17905
+ const spend = calculateOpenAITokenSpend(model, rawUsage);
17906
+ if (!spend) return;
17907
+ const mergedUsageContext = {
17908
+ ...options.usageContext || {},
17909
+ ...usageContext || {}
17910
+ };
17911
+ await options.onUsage({
17912
+ ...spend,
17913
+ source: "openai",
17914
+ lineItemType: "llm_tokens",
17915
+ operation: "chat.completions",
17916
+ requestId: requestId || null,
17917
+ usageContext: mergedUsageContext,
17918
+ rawUsage
17919
+ });
17920
+ };
17664
17921
  return async (input) => {
17665
17922
  const messages = [
17666
17923
  {
@@ -17677,80 +17934,46 @@ ${modelOutputInstruction()}`
17677
17934
  messages,
17678
17935
  response_format: { type: "json_object" }
17679
17936
  };
17680
- if (input.onTextDelta) {
17681
- payload.stream = true;
17682
- }
17683
17937
  if (typeof options.temperature === "number") {
17684
17938
  payload.temperature = options.temperature;
17685
17939
  }
17686
17940
  let lastError = null;
17687
17941
  for (let attempt = 1; attempt <= 3; attempt += 1) {
17688
17942
  try {
17689
- const response = await fetch(`${baseUrl}/chat/completions`, {
17690
- method: "POST",
17691
- headers: {
17692
- "content-type": "application/json",
17693
- authorization: `Bearer ${options.apiKey}`,
17694
- ...options.headers
17695
- },
17696
- body: JSON.stringify(payload)
17697
- });
17698
- if (!response.ok) {
17699
- const errorText = await response.text();
17700
- if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
17701
- await sleep2(500 * attempt);
17702
- continue;
17703
- }
17704
- throw new Error(
17705
- `OpenAI chat generation failed: ${response.status} ${errorText}`
17706
- );
17707
- }
17708
17943
  let raw;
17709
17944
  let text = "";
17710
- if (input.onTextDelta && response.body) {
17945
+ let usage = null;
17946
+ let requestId = null;
17947
+ if (input.onTextDelta) {
17711
17948
  const onTextDelta = input.onTextDelta;
17712
- const reader = response.body.getReader();
17713
- const decoder = new TextDecoder();
17714
- let buffer = "";
17715
- const processStreamLine = async (line) => {
17716
- const trimmedLine = line.trimEnd();
17717
- if (!trimmedLine.startsWith("data:")) return;
17718
- const data = trimmedLine.slice("data:".length).trim();
17719
- if (!data || data === "[DONE]") return;
17720
- const event = JSON.parse(data);
17721
- const delta = asRecord5(
17722
- asRecord5(event.choices?.[0])?.delta
17723
- )?.content;
17724
- const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord5(part)?.text || "").join("") : "";
17725
- if (!deltaText) return;
17949
+ const stream = await client.chat.completions.create({
17950
+ ...payload,
17951
+ stream: true,
17952
+ stream_options: { include_usage: true }
17953
+ });
17954
+ for await (const event of stream) {
17955
+ requestId = requestId || event.id || event._request_id || null;
17956
+ usage = event.usage || usage;
17957
+ const delta = event.choices?.[0]?.delta?.content;
17958
+ const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord6(part)?.text || "").join("") : "";
17959
+ if (!deltaText) continue;
17726
17960
  text += deltaText;
17727
17961
  await onTextDelta(deltaText);
17728
- };
17729
- while (true) {
17730
- const { value, done } = await reader.read();
17731
- if (done) break;
17732
- buffer += decoder.decode(value, { stream: true });
17733
- while (true) {
17734
- const lineEnd = buffer.indexOf("\n");
17735
- if (lineEnd === -1) break;
17736
- const line = buffer.slice(0, lineEnd);
17737
- buffer = buffer.slice(lineEnd + 1);
17738
- await processStreamLine(line);
17739
- }
17740
17962
  }
17741
- buffer += decoder.decode();
17742
- if (buffer.trim()) {
17743
- await processStreamLine(buffer);
17744
- }
17745
- raw = { streamed: true };
17963
+ raw = { streamed: true, model, usage, request_id: requestId };
17746
17964
  } else {
17747
- const json = await response.json();
17748
- raw = json;
17749
- const content = asRecord5(
17750
- asRecord5(json.choices?.[0])?.message
17965
+ const completion = await client.chat.completions.create(
17966
+ payload
17967
+ );
17968
+ raw = completion;
17969
+ usage = completion.usage;
17970
+ requestId = (typeof completion.id === "string" ? completion.id : null) || (typeof completion._request_id === "string" ? completion._request_id : null);
17971
+ const content = asRecord6(
17972
+ asRecord6(completion.choices?.[0])?.message
17751
17973
  )?.content;
17752
- text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord5(part)?.text || "").join("") : "";
17974
+ text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord6(part)?.text || "").join("") : "";
17753
17975
  }
17976
+ await emitUsage(usage, requestId, input.usageContext);
17754
17977
  const parsed = extractJsonObject(text);
17755
17978
  if (!parsed) {
17756
17979
  if (attempt < 3) {
@@ -17770,9 +17993,10 @@ ${modelOutputInstruction()}`
17770
17993
  };
17771
17994
  } catch (error) {
17772
17995
  lastError = error instanceof Error ? error : new Error(String(error));
17773
- if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
17996
+ const status = Number(error?.status);
17997
+ if (attempt < 3 && (Number.isFinite(status) && (status >= 500 || status === 429) || /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
17774
17998
  lastError.message
17775
- )) {
17999
+ ))) {
17776
18000
  await sleep2(500 * attempt);
17777
18001
  continue;
17778
18002
  }
@@ -17805,12 +18029,12 @@ async function withTimeout2(promise, ms, label) {
17805
18029
  }
17806
18030
  }
17807
18031
  function getActionSummary(liveDoc, jobId) {
17808
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17809
- const job = asRecord5(jobsById[jobId]);
18032
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
18033
+ const job = asRecord6(jobsById[jobId]);
17810
18034
  const summary = Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
17811
18035
  (line) => typeof line === "string"
17812
18036
  ) : [];
17813
- const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord5(event)).filter((event) => Boolean(event)) : [];
18037
+ const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord6(event)).filter((event) => Boolean(event)) : [];
17814
18038
  const traceLines = trace.map((event) => {
17815
18039
  const kind = typeof event.kind === "string" ? event.kind : "";
17816
18040
  const action = typeof event.action === "string" ? event.action : "";
@@ -17831,9 +18055,9 @@ function getActionSummary(liveDoc, jobId) {
17831
18055
  }
17832
18056
  function normalizeHeapSnapshot2(heap) {
17833
18057
  return {
17834
- entriesByPath: asRecord5(heap?.entriesByPath) || {},
17835
- listsByName: asRecord5(heap?.listsByName) || {},
17836
- variablesByName: asRecord5(heap?.variablesByName) || {},
18058
+ entriesByPath: asRecord6(heap?.entriesByPath) || {},
18059
+ listsByName: asRecord6(heap?.listsByName) || {},
18060
+ variablesByName: asRecord6(heap?.variablesByName) || {},
17837
18061
  updatedAt: typeof heap?.updatedAt === "number" ? heap.updatedAt : Date.now()
17838
18062
  };
17839
18063
  }
@@ -17868,9 +18092,9 @@ async function waitForJobOutcome(input) {
17868
18092
  input.boundaryTimestamp
17869
18093
  );
17870
18094
  lastPromptCount = prompts.length;
17871
- const messages = asArray3(asRecord5(liveDoc.conversation)?.messages);
18095
+ const messages = asArray3(asRecord6(liveDoc.conversation)?.messages);
17872
18096
  lastMessageCount = messages.length;
17873
- lastJobSummary = asRecord5(asRecord5(liveDoc.jobs)?.byId)?.[input.job.id] || null;
18097
+ lastJobSummary = asRecord6(asRecord6(liveDoc.jobs)?.byId)?.[input.job.id] || null;
17874
18098
  if (prompts.length > 0) {
17875
18099
  return { kind: "prompt", prompts, liveDoc, stdout, stderr };
17876
18100
  }
@@ -18054,7 +18278,8 @@ function jsonBlock(value) {
18054
18278
  }
18055
18279
  function buildSessionLogReport(input) {
18056
18280
  const { conversation, result, error } = input;
18057
- const systemPrompts = conversation.logTurns.flatMap(
18281
+ const logTurns = conversation.logTurns || [];
18282
+ const systemPrompts = logTurns.flatMap(
18058
18283
  (turn) => turn.iterations.map((iteration) => ({
18059
18284
  turn,
18060
18285
  iteration
@@ -18083,7 +18308,7 @@ function buildSessionLogReport(input) {
18083
18308
  "",
18084
18309
  "## Conversation"
18085
18310
  ];
18086
- for (const turn of conversation.logTurns) {
18311
+ for (const turn of logTurns) {
18087
18312
  lines.push("", `### Turn ${turn.turnNumber}: ${turn.turnId}`, "");
18088
18313
  lines.push("**User**", "");
18089
18314
  lines.push(turn.request, "");
@@ -18281,7 +18506,7 @@ async function runAgentEvalSuite(options) {
18281
18506
  promptInteractions: completed.promptInteractions,
18282
18507
  result: completed.result,
18283
18508
  heap: normalizeHeapSnapshot2(
18284
- asRecord5(
18509
+ asRecord6(
18285
18510
  cloneJson(conversation.environment.document)?.heap
18286
18511
  )
18287
18512
  ),
@@ -18391,7 +18616,7 @@ async function runAgentEvalSuite(options) {
18391
18616
  finalResult = result;
18392
18617
  } catch (error) {
18393
18618
  const failureMessage = error instanceof Error ? error.message : String(error);
18394
- const failedTurn = conversation.logTurns[conversation.logTurns.length - 1];
18619
+ const failedTurn = conversation.logTurns?.[conversation.logTurns.length - 1];
18395
18620
  if (failedTurn && !failedTurn.completed) {
18396
18621
  failedTurn.error = failureMessage;
18397
18622
  const failedIteration = latestIterationLog(failedTurn);
@@ -18524,7 +18749,7 @@ function createAgentEvalHarness(options) {
18524
18749
  }
18525
18750
  function buildCheckContext(conversation, completed, turnDir) {
18526
18751
  const liveDoc = cloneJson(conversation.environment.document);
18527
- const heap = normalizeHeapSnapshot2(asRecord5(liveDoc?.heap));
18752
+ const heap = normalizeHeapSnapshot2(asRecord6(liveDoc?.heap));
18528
18753
  return {
18529
18754
  conversation,
18530
18755
  environment: conversation.environment,
@@ -18618,7 +18843,7 @@ function createAgentEvalHarness(options) {
18618
18843
  result: resumed.result,
18619
18844
  stdout: [...pending.stdout, ...resumed.stdout],
18620
18845
  agentMessages: getJobAgentMessages(liveDoc, pending.job.id),
18621
- sessionHeap: normalizeHeapSnapshot2(asRecord5(liveDoc?.heap))
18846
+ sessionHeap: normalizeHeapSnapshot2(asRecord6(liveDoc?.heap))
18622
18847
  });
18623
18848
  const responseText = presentation.responseText || pending.finalReply || "Done.";
18624
18849
  pending.conversation.history.push({
@@ -18742,7 +18967,7 @@ function createAgentEvalHarness(options) {
18742
18967
  environmentId: conversation.environment.environmentId,
18743
18968
  domainRevision: conversation.environment.domainRevision
18744
18969
  },
18745
- heapSummary: projectHeapSummary(asRecord5(liveDoc?.heap), {
18970
+ heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
18746
18971
  focus: heapFocus
18747
18972
  }),
18748
18973
  referentSummary: projectConversationReferentSummary(liveDoc),
@@ -18764,7 +18989,14 @@ function createAgentEvalHarness(options) {
18764
18989
  history: buildHistory(conversation.history),
18765
18990
  request,
18766
18991
  attempt: 1,
18767
- tools
18992
+ tools,
18993
+ usageContext: {
18994
+ sandboxId: conversation.environment.sandboxId,
18995
+ environmentId: conversation.environment.environmentId,
18996
+ sessionId: conversation.environment.sessionId,
18997
+ subjectId: conversation.environment.subjectId,
18998
+ permissionProfileId: conversation.environment.permissionProfileId
18999
+ }
18768
19000
  }),
18769
19001
  chatTimeoutMs,
18770
19002
  `chat generation for ${conversation.label} iteration ${iteration + 1}`
@@ -18913,7 +19145,7 @@ function createAgentEvalHarness(options) {
18913
19145
  const settledLiveDoc = cloneJson(
18914
19146
  conversation.environment.document
18915
19147
  );
18916
- const sessionHeap = normalizeHeapSnapshot2(asRecord5(settledLiveDoc?.heap));
19148
+ const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
18917
19149
  const presentation = resolveJobPresentation({
18918
19150
  jobId: job.id,
18919
19151
  result: outcome.result,
@@ -19037,7 +19269,10 @@ function createAgentTester(options) {
19037
19269
  model: options.openai?.model || options.model,
19038
19270
  baseUrl: options.openai?.baseUrl,
19039
19271
  temperature: options.openai?.temperature,
19040
- headers: options.openai?.headers
19272
+ headers: options.openai?.headers,
19273
+ onUsage: async (usage) => {
19274
+ await granular.recordOpenAIUsageSpend(usage, usage.usageContext);
19275
+ }
19041
19276
  });
19042
19277
  let resolvedEnvironmentId = "environmentId" in options.target ? options.target.environmentId : null;
19043
19278
  let connectSeeded = false;