@lionhummer/constellation 0.9.2 → 0.9.4

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/README.md CHANGED
@@ -341,6 +341,8 @@ tools: {
341
341
  },
342
342
  ```
343
343
 
344
+ Work you do before the first model step, such as loading context and building the prompt, can travel with the request that starts the run: pass `telemetry: { spans, logs }` to `requests.submit`. Those spans belong to the request rather than a step: they sit at the top of the timeline at their own timestamps, before the first step (set `stepId` or `parentSpanId` to nest them elsewhere).
345
+
344
346
  The SDK records `client.processing` and one span per tool automatically. A response carries at most 512 spans, 128 logs and 1 MiB of telemetry; the SDK trims larger batches and records what it omitted. Do not put secrets or personal data in span names, attributes or log messages.
345
347
 
346
348
  ## Opening lines
@@ -361,6 +363,45 @@ if (opener) void client.openers.sentence(opener.id).then((sentence) => {
361
363
 
362
364
  Start the real request without waiting for the opener. `parseOpenerSentence(text)` applies the same checks to text you already have.
363
365
 
366
+ ## Telemetry queries
367
+
368
+ Ask aggregate questions without SQL. Fields, filters and metrics are typed per dataset.
369
+
370
+ ```ts
371
+ // How many fireworks-direct attempts failed yesterday, and why?
372
+ const failures = await client.telemetry.query({
373
+ dataset: "attempts",
374
+ from: "2026-10-15T00:00:00Z", to: "2026-10-16T00:00:00Z",
375
+ where: { offeringId: { startsWith: "fireworks-direct:" }, status: "failed" },
376
+ groupBy: ["errorCode", "httpStatus"],
377
+ metrics: ["count", "uniq:requestId"],
378
+ })
379
+
380
+ // How often did a provider say "Response too long", per day?
381
+ const mentions = await client.telemetry.query({
382
+ dataset: "attempts",
383
+ from: "2026-10-01T00:00:00Z", to: "2026-10-16T00:00:00Z",
384
+ where: { errorMessage: { contains: "Response too long" } },
385
+ groupBy: ["day"],
386
+ })
387
+ ```
388
+
389
+ Usage and costs come from the permanent billing record (`usage`, one row per settled request, or `usageAttempts`, one row per provider attempt for provider/model splits). Any range works:
390
+
391
+ ```ts
392
+ // Customer charges, supplier cost and margin per workspace and provider last month, without QA
393
+ const byProvider = await client.telemetry.query({
394
+ dataset: "usageAttempts",
395
+ from: "2026-09-01T00:00:00Z", to: "2026-10-01T00:00:00Z",
396
+ where: { workspaceId: { notIn: ["qa"] } },
397
+ groupBy: ["workspaceId", "providerId"],
398
+ metrics: ["uniq:requestId", "sum:customerCost", "sum:supplierCost", "sum:margin"],
399
+ limit: 10000,
400
+ })
401
+ ```
402
+
403
+ A scoped key only ever sees its workspace and user. Logs and usage need an unscoped key. Telemetry ranges are at most 92 days and results at most 1,000 rows; usage allows any range and 10,000 rows. Group by `minute`, `hour`, `day`, `week` or `month` for time series.
404
+
364
405
  ## Limits and timeouts
365
406
 
366
407
  | Setting | Default | Notes |
@@ -394,6 +435,7 @@ A failed provider call falls back to the next provider only before any output ha
394
435
  | `requests.getTimeline(id)`, `requests.getTimings(id)` | Trace and timing breakdown. |
395
436
  | `requests.getHttpExchange(id, attemptId)`, `requests.getHttpBody(...)` | The provider HTTP call captured for one attempt. |
396
437
  | `webhooks.verify(request, token)` | Authenticate and parse an incoming webhook. |
438
+ | `telemetry.query(query)` | Count, group and aggregate attempts, requests, logs or permanent usage, e.g. failed attempts per offering and error code, or cost per workspace and provider. Needs telemetry permission; the key's scope always applies. |
397
439
  | `telemetry.*` | Read-only reporting. Returns metadata only unless you pass `include: "content"`. Needs telemetry permission. |
398
440
  | `management.registry.*` | Edit the model registry. Needs management permission. |
399
441
  | `discover()` | The OpenAPI document for exactly what this key may call. |
package/dist/index.js CHANGED
@@ -764,6 +764,10 @@ class Constellation extends RequestClient {
764
764
  get: (id, query = {}, signal) => this.#call(`/v1/telemetry/notifications/${encodeURIComponent(id)}${queryString2(query)}`, "GET", undefined, signal),
765
765
  listEvents: (query = {}, signal) => this.#call(`/v1/telemetry/notifications/events${queryString2(query)}`, "GET", undefined, signal)
766
766
  },
767
+ query: (query, signal) => {
768
+ const iso = (value) => value instanceof Date ? value.toISOString() : value;
769
+ return this.#call("/v1/telemetry/query", "POST", { ...query, from: iso(query.from), to: iso(query.to) }, signal);
770
+ },
767
771
  logs: {
768
772
  searchSystem: (query = {}, signal) => this.#call(`/v1/telemetry/logs/system${queryString2(query)}`, "GET", undefined, signal),
769
773
  searchRequests: (query = {}, signal) => this.#call(`/v1/telemetry/logs/requests${queryString2(query)}`, "GET", undefined, signal),
@@ -28,6 +28,63 @@ export type TelemetryPage<T> = Page<T> & {
28
28
  export type TelemetryQuery = {
29
29
  include?: "content";
30
30
  };
31
+ /** Fields of each aggregate dataset, as POST /v1/telemetry/query accepts them. */
32
+ export type AttemptField = "workspaceId" | "userId" | "externalId" | "requestId" | "providerId" | "offeringId" | "model" | "requestedModel" | "status" | "fallback" | "dispatched" | "errorCode" | "faultClass" | "httpStatus" | "errorMessage";
33
+ export type AttemptNumber = "ttftMs" | "latencyMs" | "outputTokens" | "supplierCost" | "customerCost" | "httpStatus";
34
+ export type RequestField = "workspaceId" | "userId" | "externalId" | "requestId" | "requestedModel" | "status";
35
+ export type RequestNumber = "durationMs" | "supplierCost" | "customerCost";
36
+ export type LogField = "level" | "name" | "status" | "requestId" | "message";
37
+ /** Permanent billing record per settled request (unscoped telemetry keys only). */
38
+ export type UsageField = "workspaceId" | "userId" | "externalId" | "requestId" | "requestedModel" | "planId" | "status";
39
+ type UsageTotals = "supplierCost" | "customerCost" | "margin" | "inputTokens" | "cacheReadTokens" | "cacheWriteTokens" | "cacheWriteOneHourTokens" | "outputTokens" | "reasoningTokens";
40
+ export type UsageNumber = UsageTotals | "durationMs";
41
+ /** Permanent billing record per provider attempt; `status` is the attempt's, `requestStatus` the request's. */
42
+ export type UsageAttemptField = Exclude<UsageField, "status"> | "requestStatus" | "providerId" | "model" | "providerModel" | "status";
43
+ export type UsageAttemptNumber = UsageTotals;
44
+ export type AggregateTimeGroup = "minute" | "hour" | "day" | "week" | "month";
45
+ export type AggregateScalar = string | number | boolean;
46
+ /** A value, any of several values, or operators combined with AND. `contains` is case-insensitive. */
47
+ export type AggregateFilter = AggregateScalar | AggregateScalar[] | {
48
+ eq?: AggregateScalar;
49
+ in?: AggregateScalar[];
50
+ not?: AggregateScalar;
51
+ notIn?: AggregateScalar[];
52
+ contains?: string;
53
+ startsWith?: string;
54
+ gte?: number;
55
+ gt?: number;
56
+ lte?: number;
57
+ lt?: number;
58
+ /** true: the field has a value; false: it is empty. */
59
+ exists?: boolean;
60
+ };
61
+ export type AggregateMetric<N extends string, F extends string> = "count" | `uniq:${F}` | `${"sum" | "avg" | "min" | "max" | "p50" | "p90" | "p95" | "p99"}:${N}`;
62
+ type AggregateShape<D extends string, F extends string, N extends string> = {
63
+ dataset: D;
64
+ /** Inclusive start and exclusive end; at most 92 days apart (usage datasets: any range). */
65
+ from: string | Date;
66
+ to: string | Date;
67
+ where?: Partial<Record<F | N, AggregateFilter>>;
68
+ /** At most four. Numeric fields other than httpStatus are aggregated, not grouped. */
69
+ groupBy?: (F | AggregateTimeGroup)[];
70
+ /** One to eight; default ["count"]. */
71
+ metrics?: AggregateMetric<N, F>[];
72
+ orderBy?: {
73
+ by: string;
74
+ descending?: boolean;
75
+ };
76
+ /** Default 100, at most 1,000 rows (usage datasets: 10,000). */
77
+ limit?: number;
78
+ };
79
+ /** A structured aggregate over telemetry facts or permanent usage. The key's workspace/user scope always applies; logs and usage need an unscoped key. */
80
+ export type AggregateQuery = AggregateShape<"attempts", AttemptField, AttemptNumber> | AggregateShape<"requests", RequestField, RequestNumber> | AggregateShape<"logs", LogField, never> | AggregateShape<"usage", UsageField, UsageNumber> | AggregateShape<"usageAttempts", UsageAttemptField, UsageAttemptNumber>;
81
+ export type AggregateResult = {
82
+ /** Groupings first, then metrics, in request order. */
83
+ columns: string[];
84
+ rows: Record<string, Json>[];
85
+ /** More groups matched than `limit`. */
86
+ truncated: boolean;
87
+ };
31
88
  export type TelemetryReadOptions = TelemetryQuery & {
32
89
  signal?: AbortSignal;
33
90
  };
@@ -191,6 +248,8 @@ export declare class Constellation extends RequestClient {
191
248
  after?: number;
192
249
  }, signal?: AbortSignal) => Promise<Json>;
193
250
  };
251
+ /** Count, group and aggregate attempts, requests or logs, e.g. failed attempts per offering and error code. */
252
+ query: (query: AggregateQuery, signal?: AbortSignal) => Promise<AggregateResult>;
194
253
  logs: {
195
254
  searchSystem: (query?: LogSearchQuery & TelemetryQuery, signal?: AbortSignal) => Promise<Page<Json>>;
196
255
  searchRequests: (query?: Omit<LogSearchQuery, "level" | "levelNot" | "target" | "targetNot"> & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionhummer/constellation",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "TypeScript client for durable model requests and model events",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",