@usewind/node 0.1.0 → 0.1.2

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.d.cts CHANGED
@@ -17,6 +17,21 @@ interface ChatMessage {
17
17
  content: unknown;
18
18
  [k: string]: unknown;
19
19
  }
20
+ /** OpenAI-style function tool. Wind maps it onto Anthropic and Gemini for you. */
21
+ interface ChatTool {
22
+ type: 'function';
23
+ function: {
24
+ name: string;
25
+ description?: string;
26
+ parameters?: Record<string, unknown>;
27
+ };
28
+ }
29
+ type ChatToolChoice = 'auto' | 'none' | 'required' | {
30
+ type: 'function';
31
+ function: {
32
+ name: string;
33
+ };
34
+ };
20
35
  /** Present on every successful `POST /v1/ai/*` response. */
21
36
  interface WindBlock {
22
37
  feature: string | null;
@@ -49,6 +64,14 @@ interface ChatParams {
49
64
  /** Optional — hold cap resolves request → feature → app default → model ceiling. */
50
65
  max_tokens?: number;
51
66
  temperature?: number;
67
+ /**
68
+ * Function tools, OpenAI format, for every provider. Send the assistant
69
+ * message back unchanged (including any `extra_content` on its tool calls —
70
+ * Gemini needs it), then one `{ role: 'tool', tool_call_id, content }` per call.
71
+ */
72
+ tools?: ChatTool[];
73
+ tool_choice?: ChatToolChoice;
74
+ parallel_tool_calls?: boolean;
52
75
  /** Prefer `.chatStream()` over setting this directly. */
53
76
  stream?: boolean;
54
77
  /** Defaults to `crypto.randomUUID()`; the same value is reused for every internal retry. */
@@ -56,11 +79,36 @@ interface ChatParams {
56
79
  signal?: AbortSignal;
57
80
  }
58
81
  interface ChatStreamChunk {
59
- /** Raw provider SSE `data:` payload, JSON-parsed. */
82
+ /**
83
+ * One provider chunk, OpenAI shape (`choices[0].delta.content` /
84
+ * `.tool_calls`), JSON-parsed. `null` on the final, settlement-only chunk.
85
+ */
60
86
  delta: unknown;
61
- /** The final chunk of a stream carries the settled `wind` block; `null` on all others. */
87
+ /** The final chunk of a stream carries the settled `wind` block; `null` on all others, and on a final chunk that isn't `settled`. */
62
88
  wind: WindBlock | null;
89
+ /** Final token counts — on the final chunk only, when `settled`. */
90
+ usage?: ChatUsage;
91
+ /** How the call's charge stands — on the final chunk only. */
92
+ settlement?: ChatSettlement;
93
+ /**
94
+ * The call's `Idempotency-Key` — on the final chunk only. A `pending` call's
95
+ * `request.settled` webhook carries it as `request_id`.
96
+ */
97
+ idempotencyKey?: string;
63
98
  }
99
+ /**
100
+ * - `settled`: charged on the provider's own token counts; `wind` and `usage`
101
+ * are filled in.
102
+ * - `estimated`: the stream ended before the provider reported usage, and it
103
+ * keeps no record to fetch it from (OpenAI, Anthropic, Google direct), so it
104
+ * was charged on Wind's count of the tokens with the provider's tokenizer.
105
+ * `wind` and `usage` are filled in from that count.
106
+ * - `pending`: the stream ended before the provider reported usage, but the
107
+ * provider keeps a record (OpenRouter), so the charge isn't known yet.
108
+ * `wind` and `usage` are null; the user's hold stays reserved until Wind
109
+ * settles it, and the result arrives as a `request.settled` webhook.
110
+ */
111
+ type ChatSettlement = 'settled' | 'estimated' | 'pending';
64
112
  /** 'frozen' — Wind's own risk layer, independent of anything you or the user configured; clears when the user reconnects. */
65
113
  type ConnectionStatus = 'active' | 'exhausted' | 'revoked' | 'frozen';
66
114
  interface Connection {
@@ -213,6 +261,12 @@ interface RawRequest {
213
261
  /** Use HTTP Basic client credentials for `Authorization`. */
214
262
  basic?: boolean;
215
263
  signal?: AbortSignal;
264
+ /**
265
+ * When the response is a 2xx `text/event-stream`, return its unread
266
+ * `ReadableStream` as `body` instead of buffering it — `.chatStream()`
267
+ * parses the SSE events as they arrive.
268
+ */
269
+ stream?: boolean;
216
270
  }
217
271
  interface RawResponse<T = unknown> {
218
272
  status: number;
@@ -592,7 +646,7 @@ declare class Wind {
592
646
  register: (appId: string, feature: Feature) => Promise<Feature>;
593
647
  update: (appId: string, slug: string, patch: Partial<Feature>) => Promise<Feature>;
594
648
  };
595
- /** Verify a webhook's `X-Wind-Signature` and return the typed event. Needs the raw body. */
649
+ /** Verify a webhook's `Wind-Signature` and return the typed event. Needs the raw body. */
596
650
  verify(input: {
597
651
  payload: string | Buffer;
598
652
  signature: string;
@@ -606,4 +660,4 @@ declare function createWind(config?: Partial<WindConfig>): Wind;
606
660
  */
607
661
  declare const wind: Wind;
608
662
 
609
- export { type Account, AllocationExhaustedError, type AllocationRequest, type AllocationRequestParams, AppSuspendedError, type ChatMessage, type ChatParams, type ChatResult, type ChatStreamChunk, type ChatUsage, type Chattable, type ConnectTransaction, type Connection, ConnectionFrozenError, ConnectionRevokedError, ConnectionScope, type ConnectionStatus, type Feature, FeatureCostExceededError, FeatureModelNotConfiguredError, FeatureScope, type Grant, type GrantStore, IdempotencyKeyReusedError, ModelNotAllowedError, ProviderError, RateLimitedError, type RevokeResult, RiskThrottledError, type RoutesOptions, TokenExpiredError, type TokenResponse, type TransactionStore, type WebhookEvent, Wind, WindApiError, type WindBlock, type WindConfig, WindConfigError, WindError, type WindErrorEnvelope, type WindErrorInit, WindNotConnectedError, WindNotImplementedError, WindSignatureError, createWind, memoryGrantStore, wind };
663
+ export { type Account, AllocationExhaustedError, type AllocationRequest, type AllocationRequestParams, AppSuspendedError, type ChatMessage, type ChatParams, type ChatResult, type ChatSettlement, type ChatStreamChunk, type ChatTool, type ChatToolChoice, type ChatUsage, type Chattable, type ConnectTransaction, type Connection, ConnectionFrozenError, ConnectionRevokedError, ConnectionScope, type ConnectionStatus, type Feature, FeatureCostExceededError, FeatureModelNotConfiguredError, FeatureScope, type Grant, type GrantStore, IdempotencyKeyReusedError, ModelNotAllowedError, ProviderError, RateLimitedError, type RevokeResult, RiskThrottledError, type RoutesOptions, TokenExpiredError, type TokenResponse, type TransactionStore, type WebhookEvent, Wind, WindApiError, type WindBlock, type WindConfig, WindConfigError, WindError, type WindErrorEnvelope, type WindErrorInit, WindNotConnectedError, WindNotImplementedError, WindSignatureError, createWind, memoryGrantStore, wind };
package/dist/index.d.ts CHANGED
@@ -17,6 +17,21 @@ interface ChatMessage {
17
17
  content: unknown;
18
18
  [k: string]: unknown;
19
19
  }
20
+ /** OpenAI-style function tool. Wind maps it onto Anthropic and Gemini for you. */
21
+ interface ChatTool {
22
+ type: 'function';
23
+ function: {
24
+ name: string;
25
+ description?: string;
26
+ parameters?: Record<string, unknown>;
27
+ };
28
+ }
29
+ type ChatToolChoice = 'auto' | 'none' | 'required' | {
30
+ type: 'function';
31
+ function: {
32
+ name: string;
33
+ };
34
+ };
20
35
  /** Present on every successful `POST /v1/ai/*` response. */
21
36
  interface WindBlock {
22
37
  feature: string | null;
@@ -49,6 +64,14 @@ interface ChatParams {
49
64
  /** Optional — hold cap resolves request → feature → app default → model ceiling. */
50
65
  max_tokens?: number;
51
66
  temperature?: number;
67
+ /**
68
+ * Function tools, OpenAI format, for every provider. Send the assistant
69
+ * message back unchanged (including any `extra_content` on its tool calls —
70
+ * Gemini needs it), then one `{ role: 'tool', tool_call_id, content }` per call.
71
+ */
72
+ tools?: ChatTool[];
73
+ tool_choice?: ChatToolChoice;
74
+ parallel_tool_calls?: boolean;
52
75
  /** Prefer `.chatStream()` over setting this directly. */
53
76
  stream?: boolean;
54
77
  /** Defaults to `crypto.randomUUID()`; the same value is reused for every internal retry. */
@@ -56,11 +79,36 @@ interface ChatParams {
56
79
  signal?: AbortSignal;
57
80
  }
58
81
  interface ChatStreamChunk {
59
- /** Raw provider SSE `data:` payload, JSON-parsed. */
82
+ /**
83
+ * One provider chunk, OpenAI shape (`choices[0].delta.content` /
84
+ * `.tool_calls`), JSON-parsed. `null` on the final, settlement-only chunk.
85
+ */
60
86
  delta: unknown;
61
- /** The final chunk of a stream carries the settled `wind` block; `null` on all others. */
87
+ /** The final chunk of a stream carries the settled `wind` block; `null` on all others, and on a final chunk that isn't `settled`. */
62
88
  wind: WindBlock | null;
89
+ /** Final token counts — on the final chunk only, when `settled`. */
90
+ usage?: ChatUsage;
91
+ /** How the call's charge stands — on the final chunk only. */
92
+ settlement?: ChatSettlement;
93
+ /**
94
+ * The call's `Idempotency-Key` — on the final chunk only. A `pending` call's
95
+ * `request.settled` webhook carries it as `request_id`.
96
+ */
97
+ idempotencyKey?: string;
63
98
  }
99
+ /**
100
+ * - `settled`: charged on the provider's own token counts; `wind` and `usage`
101
+ * are filled in.
102
+ * - `estimated`: the stream ended before the provider reported usage, and it
103
+ * keeps no record to fetch it from (OpenAI, Anthropic, Google direct), so it
104
+ * was charged on Wind's count of the tokens with the provider's tokenizer.
105
+ * `wind` and `usage` are filled in from that count.
106
+ * - `pending`: the stream ended before the provider reported usage, but the
107
+ * provider keeps a record (OpenRouter), so the charge isn't known yet.
108
+ * `wind` and `usage` are null; the user's hold stays reserved until Wind
109
+ * settles it, and the result arrives as a `request.settled` webhook.
110
+ */
111
+ type ChatSettlement = 'settled' | 'estimated' | 'pending';
64
112
  /** 'frozen' — Wind's own risk layer, independent of anything you or the user configured; clears when the user reconnects. */
65
113
  type ConnectionStatus = 'active' | 'exhausted' | 'revoked' | 'frozen';
66
114
  interface Connection {
@@ -213,6 +261,12 @@ interface RawRequest {
213
261
  /** Use HTTP Basic client credentials for `Authorization`. */
214
262
  basic?: boolean;
215
263
  signal?: AbortSignal;
264
+ /**
265
+ * When the response is a 2xx `text/event-stream`, return its unread
266
+ * `ReadableStream` as `body` instead of buffering it — `.chatStream()`
267
+ * parses the SSE events as they arrive.
268
+ */
269
+ stream?: boolean;
216
270
  }
217
271
  interface RawResponse<T = unknown> {
218
272
  status: number;
@@ -592,7 +646,7 @@ declare class Wind {
592
646
  register: (appId: string, feature: Feature) => Promise<Feature>;
593
647
  update: (appId: string, slug: string, patch: Partial<Feature>) => Promise<Feature>;
594
648
  };
595
- /** Verify a webhook's `X-Wind-Signature` and return the typed event. Needs the raw body. */
649
+ /** Verify a webhook's `Wind-Signature` and return the typed event. Needs the raw body. */
596
650
  verify(input: {
597
651
  payload: string | Buffer;
598
652
  signature: string;
@@ -606,4 +660,4 @@ declare function createWind(config?: Partial<WindConfig>): Wind;
606
660
  */
607
661
  declare const wind: Wind;
608
662
 
609
- export { type Account, AllocationExhaustedError, type AllocationRequest, type AllocationRequestParams, AppSuspendedError, type ChatMessage, type ChatParams, type ChatResult, type ChatStreamChunk, type ChatUsage, type Chattable, type ConnectTransaction, type Connection, ConnectionFrozenError, ConnectionRevokedError, ConnectionScope, type ConnectionStatus, type Feature, FeatureCostExceededError, FeatureModelNotConfiguredError, FeatureScope, type Grant, type GrantStore, IdempotencyKeyReusedError, ModelNotAllowedError, ProviderError, RateLimitedError, type RevokeResult, RiskThrottledError, type RoutesOptions, TokenExpiredError, type TokenResponse, type TransactionStore, type WebhookEvent, Wind, WindApiError, type WindBlock, type WindConfig, WindConfigError, WindError, type WindErrorEnvelope, type WindErrorInit, WindNotConnectedError, WindNotImplementedError, WindSignatureError, createWind, memoryGrantStore, wind };
663
+ export { type Account, AllocationExhaustedError, type AllocationRequest, type AllocationRequestParams, AppSuspendedError, type ChatMessage, type ChatParams, type ChatResult, type ChatSettlement, type ChatStreamChunk, type ChatTool, type ChatToolChoice, type ChatUsage, type Chattable, type ConnectTransaction, type Connection, ConnectionFrozenError, ConnectionRevokedError, ConnectionScope, type ConnectionStatus, type Feature, FeatureCostExceededError, FeatureModelNotConfiguredError, FeatureScope, type Grant, type GrantStore, IdempotencyKeyReusedError, ModelNotAllowedError, ProviderError, RateLimitedError, type RevokeResult, RiskThrottledError, type RoutesOptions, TokenExpiredError, type TokenResponse, type TransactionStore, type WebhookEvent, Wind, WindApiError, type WindBlock, type WindConfig, WindConfigError, WindError, type WindErrorEnvelope, type WindErrorInit, WindNotConnectedError, WindNotImplementedError, WindSignatureError, createWind, memoryGrantStore, wind };
package/dist/index.js CHANGED
@@ -234,6 +234,9 @@ var RawClient = class {
234
234
  signal: req.signal
235
235
  });
236
236
  const requestId = res.headers.get("x-request-id");
237
+ if (req.stream && res.ok && res.body && (res.headers.get("content-type") ?? "").includes("text/event-stream")) {
238
+ return { status: res.status, headers: res.headers, body: res.body, requestId };
239
+ }
237
240
  let parsed = void 0;
238
241
  const text = await res.text();
239
242
  if (text) {
@@ -288,7 +291,8 @@ var RawClient = class {
288
291
  bearer: args.accessToken,
289
292
  headers,
290
293
  body: args.body,
291
- signal: args.signal
294
+ signal: args.signal,
295
+ stream: isStreamBody(args.body)
292
296
  });
293
297
  }
294
298
  /** `POST /v1/ai/features/{slug}`. */
@@ -299,7 +303,8 @@ var RawClient = class {
299
303
  bearer: args.accessToken,
300
304
  headers: { "Idempotency-Key": args.idempotencyKey },
301
305
  body: args.body,
302
- signal: args.signal
306
+ signal: args.signal,
307
+ stream: isStreamBody(args.body)
303
308
  });
304
309
  }
305
310
  // ── Management (client-credentials) ────────────────────────────────────────
@@ -362,6 +367,9 @@ var RawClient = class {
362
367
  return this.request({ method: "POST", path, ...init });
363
368
  }
364
369
  };
370
+ function isStreamBody(body) {
371
+ return typeof body === "object" && body !== null && body.stream === true;
372
+ }
365
373
 
366
374
  // src/tokens.ts
367
375
  var ACCESS_TOKEN_SKEW_MS = 6e4;
@@ -620,6 +628,9 @@ function requestBody(params) {
620
628
  messages: params.messages,
621
629
  ...params.max_tokens !== void 0 && { max_tokens: params.max_tokens },
622
630
  ...params.temperature !== void 0 && { temperature: params.temperature },
631
+ ...params.tools !== void 0 && { tools: params.tools },
632
+ ...params.tool_choice !== void 0 && { tool_choice: params.tool_choice },
633
+ ...params.parallel_tool_calls !== void 0 && { parallel_tool_calls: params.parallel_tool_calls },
623
634
  ...params.stream !== void 0 && { stream: params.stream }
624
635
  };
625
636
  }
@@ -666,13 +677,60 @@ async function* billedStream(ctx, userId, params, call) {
666
677
  if (res.status < 200 || res.status >= 300) {
667
678
  throw WindError.fromResponse(res.status, res.body, { requestId: res.requestId });
668
679
  }
669
- const contentType = res.headers.get("content-type") ?? "";
670
- if (!contentType.includes("text/event-stream")) {
680
+ if (!(res.body instanceof ReadableStream)) {
671
681
  const result = res.body;
672
- yield { delta: result, wind: result.wind };
682
+ yield { delta: result, wind: result.wind ?? null, usage: result.usage, settlement: "settled", idempotencyKey };
673
683
  return;
674
684
  }
675
- yield { delta: res.body, wind: null };
685
+ for await (const event of sseEvents(res.body)) {
686
+ if (event === "[DONE]") return;
687
+ let data;
688
+ try {
689
+ data = JSON.parse(event);
690
+ } catch {
691
+ continue;
692
+ }
693
+ if (data?.wind_final) {
694
+ yield finalChunk(data.wind_final, idempotencyKey);
695
+ } else if (data?.error) {
696
+ const err = data.error.error ?? data.error;
697
+ const status = err.code === "provider_error" ? 502 : 403;
698
+ throw WindError.fromResponse(status, { error: err }, { requestId: res.requestId });
699
+ } else if (data?.streamed) {
700
+ yield finalChunk(data, idempotencyKey);
701
+ } else {
702
+ yield { delta: data, wind: null };
703
+ }
704
+ }
705
+ }
706
+ async function* sseEvents(stream) {
707
+ const reader = stream.getReader();
708
+ const decoder = new TextDecoder();
709
+ let buffer = "";
710
+ try {
711
+ while (true) {
712
+ const { done, value } = await reader.read();
713
+ if (done) break;
714
+ buffer += decoder.decode(value, { stream: true });
715
+ const lines = buffer.split("\n");
716
+ buffer = lines.pop() ?? "";
717
+ for (const line of lines) {
718
+ if (line.startsWith("data:")) yield line.slice(5).trim();
719
+ }
720
+ }
721
+ if (buffer.startsWith("data:")) yield buffer.slice(5).trim();
722
+ } finally {
723
+ reader.releaseLock();
724
+ }
725
+ }
726
+ function finalChunk(final, idempotencyKey) {
727
+ return {
728
+ delta: null,
729
+ wind: final.wind ?? null,
730
+ usage: final.usage ?? void 0,
731
+ settlement: final.settlement ?? "settled",
732
+ idempotencyKey
733
+ };
676
734
  }
677
735
  var TOLERANCE_S = 300;
678
736
  var WebhookVerifier = class {
@@ -995,7 +1053,7 @@ var Wind = class {
995
1053
  update: (appId, slug, patch) => this.raw.updateFeature(appId, slug, patch)
996
1054
  };
997
1055
  // ── webhooks ─────────────────────────────────────────────────────────────
998
- /** Verify a webhook's `X-Wind-Signature` and return the typed event. Needs the raw body. */
1056
+ /** Verify a webhook's `Wind-Signature` and return the typed event. Needs the raw body. */
999
1057
  verify(input) {
1000
1058
  return this.#verifier.verify(input);
1001
1059
  }