@anthropic-ai/foundry-sdk 0.3.1 → 0.4.1

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.
Files changed (112) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/client.d.mts +1 -0
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +1 -0
  5. package/client.d.ts.map +1 -1
  6. package/client.js +4 -0
  7. package/client.js.map +1 -1
  8. package/client.mjs +4 -0
  9. package/client.mjs.map +1 -1
  10. package/core/error.d.mts.map +1 -1
  11. package/core/error.d.ts.map +1 -1
  12. package/core/error.mjs.map +1 -1
  13. package/core/middleware.d.mts.map +1 -1
  14. package/core/middleware.d.ts.map +1 -1
  15. package/core/middleware.mjs.map +1 -1
  16. package/core/pagination.d.mts.map +1 -1
  17. package/core/pagination.d.ts.map +1 -1
  18. package/core/pagination.mjs.map +1 -1
  19. package/core/streaming.d.mts.map +1 -1
  20. package/core/streaming.d.ts.map +1 -1
  21. package/core/streaming.mjs.map +1 -1
  22. package/index.d.mts.map +1 -1
  23. package/index.d.ts.map +1 -1
  24. package/index.mjs.map +1 -1
  25. package/internal/constants.d.mts.map +1 -1
  26. package/internal/constants.d.ts.map +1 -1
  27. package/internal/constants.js +0 -5
  28. package/internal/constants.js.map +1 -1
  29. package/internal/constants.mjs +0 -5
  30. package/internal/constants.mjs.map +1 -1
  31. package/internal/decoders/jsonl.d.mts.map +1 -1
  32. package/internal/decoders/jsonl.d.ts.map +1 -1
  33. package/internal/decoders/jsonl.mjs.map +1 -1
  34. package/internal/decoders/line.mjs.map +1 -1
  35. package/internal/headers.d.mts +7 -0
  36. package/internal/headers.d.mts.map +1 -1
  37. package/internal/headers.d.ts +7 -0
  38. package/internal/headers.d.ts.map +1 -1
  39. package/internal/headers.js +45 -4
  40. package/internal/headers.js.map +1 -1
  41. package/internal/headers.mjs +43 -3
  42. package/internal/headers.mjs.map +1 -1
  43. package/internal/parse.d.mts.map +1 -1
  44. package/internal/parse.d.ts.map +1 -1
  45. package/internal/parse.js +10 -1
  46. package/internal/parse.js.map +1 -1
  47. package/internal/parse.mjs +10 -1
  48. package/internal/parse.mjs.map +1 -1
  49. package/internal/qs/formats.d.mts.map +1 -1
  50. package/internal/qs/formats.d.ts.map +1 -1
  51. package/internal/qs/index.d.mts +2 -2
  52. package/internal/qs/index.d.mts.map +1 -1
  53. package/internal/qs/index.d.ts +2 -2
  54. package/internal/qs/index.d.ts.map +1 -1
  55. package/internal/qs/index.mjs.map +1 -1
  56. package/internal/qs/stringify.d.mts.map +1 -1
  57. package/internal/qs/stringify.d.ts.map +1 -1
  58. package/internal/qs/stringify.mjs.map +1 -1
  59. package/internal/qs/utils.d.mts.map +1 -1
  60. package/internal/qs/utils.d.ts.map +1 -1
  61. package/internal/qs/utils.mjs.map +1 -1
  62. package/internal/request-options.d.mts.map +1 -1
  63. package/internal/request-options.d.ts.map +1 -1
  64. package/internal/request-signal.d.mts +4 -0
  65. package/internal/request-signal.d.mts.map +1 -0
  66. package/internal/request-signal.d.ts +4 -0
  67. package/internal/request-signal.d.ts.map +1 -0
  68. package/internal/request-signal.js +50 -0
  69. package/internal/request-signal.js.map +1 -0
  70. package/internal/request-signal.mjs +45 -0
  71. package/internal/request-signal.mjs.map +1 -0
  72. package/internal/shims.d.mts +6 -0
  73. package/internal/shims.d.mts.map +1 -1
  74. package/internal/shims.d.ts +6 -0
  75. package/internal/shims.d.ts.map +1 -1
  76. package/internal/stainless-helper-header.d.mts +60 -0
  77. package/internal/stainless-helper-header.d.mts.map +1 -0
  78. package/internal/stainless-helper-header.d.ts +60 -0
  79. package/internal/stainless-helper-header.d.ts.map +1 -0
  80. package/internal/stainless-helper-header.js +91 -0
  81. package/internal/stainless-helper-header.js.map +1 -0
  82. package/internal/stainless-helper-header.mjs +83 -0
  83. package/internal/stainless-helper-header.mjs.map +1 -0
  84. package/internal/to-file.d.mts.map +1 -1
  85. package/internal/to-file.d.ts.map +1 -1
  86. package/internal/to-file.mjs.map +1 -1
  87. package/internal/types.d.mts +4 -4
  88. package/internal/types.d.mts.map +1 -1
  89. package/internal/types.d.ts +4 -4
  90. package/internal/types.d.ts.map +1 -1
  91. package/internal/uploads.d.mts.map +1 -1
  92. package/internal/uploads.d.ts.map +1 -1
  93. package/internal/uploads.mjs.map +1 -1
  94. package/internal/utils/backoff.mjs.map +1 -1
  95. package/internal/utils/base64.mjs.map +1 -1
  96. package/internal/utils/log.d.mts.map +1 -1
  97. package/internal/utils/log.d.ts.map +1 -1
  98. package/internal/utils/log.mjs.map +1 -1
  99. package/internal/utils/path.mjs.map +1 -1
  100. package/internal/utils/query.mjs.map +1 -1
  101. package/internal/utils/values.mjs.map +1 -1
  102. package/internal/utils.d.mts.map +1 -1
  103. package/internal/utils.d.ts.map +1 -1
  104. package/internal/utils.mjs.map +1 -1
  105. package/package.json +18 -2
  106. package/src/client.ts +5 -0
  107. package/src/internal/constants.ts +0 -5
  108. package/src/internal/headers.ts +47 -4
  109. package/src/internal/message-stream-utils.ts +30 -0
  110. package/src/internal/parse.ts +10 -1
  111. package/src/internal/request-signal.ts +68 -0
  112. package/src/internal/stainless-helper-header.ts +122 -0
@@ -29,7 +29,9 @@ export type NullableHeaders = {
29
29
  nulls: Set<string>;
30
30
  };
31
31
 
32
- function* iterateHeaders(headers: HeadersLike): IterableIterator<readonly [string, string | null]> {
32
+ function* iterateHeaders(
33
+ headers: HeadersLike,
34
+ ): IterableIterator<readonly [string, string | null | ClearSentinel]> {
33
35
  if (!headers) return;
34
36
 
35
37
  if (brand_privateNullableHeaders in headers) {
@@ -60,16 +62,43 @@ function* iterateHeaders(headers: HeadersLike): IterableIterator<readonly [strin
60
62
  if (value === undefined) continue;
61
63
 
62
64
  // Objects keys always overwrite older headers, they never append.
63
- // Yield a null to clear the header before adding the new values.
65
+ // Yield the clear sentinel before adding the new values, so the
66
+ // consumer can tell this synthetic "clear-before-set" apart from a
67
+ // user's explicit `null` (= remove).
64
68
  if (shouldClear && !didClear) {
65
69
  didClear = true;
66
- yield [name, null];
70
+ yield [name, clearSentinel];
67
71
  }
68
72
  yield [name, value];
69
73
  }
70
74
  }
71
75
  }
72
76
 
77
+ /** Distinguishes iterateHeaders' synthetic clear-before-set from a user `null`. */
78
+ const clearSentinel = Symbol('clear');
79
+ type ClearSentinel = typeof clearSentinel;
80
+
81
+ /**
82
+ * Headers whose values accumulate across {@link buildHeaders} sources instead
83
+ * of the later source's value replacing the earlier one. Values are
84
+ * comma-appended (deduplicated, order-preserving) into a single header line.
85
+ */
86
+ export const APPEND_HEADERS: ReadonlySet<string> = new Set(['x-stainless-helper']);
87
+
88
+ export const appendHeaderValue = (existing: string | null, addition: string): string => {
89
+ const tokens =
90
+ existing ?
91
+ existing
92
+ .split(',')
93
+ .map((t) => t.trim())
94
+ .filter(Boolean)
95
+ : [];
96
+ for (const tok of addition.split(',').map((t) => t.trim())) {
97
+ if (tok && !tokens.includes(tok)) tokens.push(tok);
98
+ }
99
+ return tokens.join(', ');
100
+ };
101
+
73
102
  export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
74
103
  const targetHeaders = new Headers();
75
104
  const nullHeaders = new Set<string>();
@@ -77,9 +106,23 @@ export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
77
106
  const seenHeaders = new Set<string>();
78
107
  for (const [name, value] of iterateHeaders(headers)) {
79
108
  const lowerName = name.toLowerCase();
80
- if (!seenHeaders.has(lowerName)) {
109
+ if (APPEND_HEADERS.has(lowerName)) {
110
+ // Accumulating headers ignore the synthetic clear-before-set; an
111
+ // explicit `null` (any source shape) is honored as removal.
112
+ if (value === clearSentinel) continue;
113
+ if (value === null) {
114
+ targetHeaders.delete(name);
115
+ nullHeaders.add(lowerName);
116
+ } else {
117
+ targetHeaders.set(name, appendHeaderValue(targetHeaders.get(name), value));
118
+ nullHeaders.delete(lowerName);
119
+ }
120
+ continue;
121
+ }
122
+ if (value === clearSentinel || !seenHeaders.has(lowerName)) {
81
123
  targetHeaders.delete(name);
82
124
  seenHeaders.add(lowerName);
125
+ if (value === clearSentinel) continue;
83
126
  }
84
127
  if (value === null) {
85
128
  targetHeaders.delete(name);
@@ -0,0 +1,30 @@
1
+ import { partialParse } from '../_vendor/partial-json-parser/parser';
2
+
3
+ export const JSON_BUF_PROPERTY = '__json_buf';
4
+
5
+ /**
6
+ * Copies a tool-use block with an updated `__json_buf`, installing `.input` as
7
+ * a memoized getter so the partial-JSON parse happens on first read instead of
8
+ * on every delta.
9
+ */
10
+ export function withLazyInput<T extends { input: unknown }>(prev: T, jsonBuf: string): T {
11
+ const next = {} as T;
12
+ for (const key of Object.keys(prev) as (keyof T)[]) {
13
+ if (key !== 'input') next[key] = prev[key];
14
+ }
15
+ Object.defineProperty(next, JSON_BUF_PROPERTY, { value: jsonBuf, enumerable: false, writable: true });
16
+ let input: unknown;
17
+ let parsed = false;
18
+ Object.defineProperty(next, 'input', {
19
+ enumerable: true,
20
+ configurable: true,
21
+ get() {
22
+ if (!parsed) {
23
+ input = jsonBuf ? partialParse(jsonBuf) : {};
24
+ parsed = true;
25
+ }
26
+ return input;
27
+ },
28
+ });
29
+ return next;
30
+ }
@@ -4,6 +4,7 @@ import type { FinalRequestOptions } from './request-options';
4
4
  import { Stream } from '../core/streaming';
5
5
  import { type BaseAnthropic } from '../client';
6
6
  import { formatRequestDetails, loggerFor } from './utils/log';
7
+ import { releaseRequestSignal } from './request-signal';
7
8
  import type { AbstractPage } from '../core/pagination';
8
9
 
9
10
  export type APIResponseProps = {
@@ -55,7 +56,15 @@ export async function defaultParseResponse<T>(
55
56
 
56
57
  const text = await response.text();
57
58
  return text as unknown as T;
58
- })();
59
+ })().finally(() => {
60
+ // The body is settled (or parsing threw), so the caller-signal abort
61
+ // listener has nothing left to cancel. Streams release in their own
62
+ // teardown; a raw Response (`__binaryResponse`) keeps the listener so
63
+ // aborting an in-flight download still works.
64
+ if (!props.options.stream && !props.options.__binaryResponse) {
65
+ releaseRequestSignal(props.controller);
66
+ }
67
+ });
59
68
  loggerFor(client).debug(
60
69
  `[${requestLogID}] response parsed`,
61
70
  formatRequestDetails({
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tracks the removal of the per-request abort listener that
3
+ * `fetchWithTimeout` attaches to a caller-provided signal, so the listener's
4
+ * lifetime matches the request instead of the signal.
5
+ *
6
+ * Without removal, a long-lived signal (e.g. one AbortController reused for
7
+ * a whole session) accumulates one `{ once: true }` listener plus its bound
8
+ * AbortController per HTTP attempt until the signal fires or is collected,
9
+ * and Node warns at the 11th listener. The listener must survive until the
10
+ * response body is settled - removing it when fetch resolves (headers) would
11
+ * break aborting an in-flight body read - so the code that finishes the body
12
+ * (response parsing, stream teardown, retry/error handling) calls
13
+ * `releaseRequestSignal` with the request's controller.
14
+ */
15
+ const cleanups = new WeakMap<AbortController, () => void>();
16
+
17
+ // Backstop for requests that never reach an explicit release point: a caller
18
+ // that partially iterates a stream (or receives a raw binary Response) and
19
+ // drops the reference never completes, errors, or cancels it, so the only
20
+ // remaining lifecycle event is the response being collected. Finalization
21
+ // timing is GC-driven and not guaranteed, so this only bounds abandonment -
22
+ // the explicit release calls stay the primary cleanup. The held value must
23
+ // not reference the response, or it could never be collected. Typed
24
+ // structurally because the compiler lib is es2020.
25
+ type AbandonmentRegistry = {
26
+ register(target: object, heldValue: AbortController, token: object): void;
27
+ unregister(token: object): void;
28
+ };
29
+
30
+ const registry: AbandonmentRegistry | null =
31
+ typeof (globalThis as any).FinalizationRegistry === 'function' ?
32
+ new (globalThis as any).FinalizationRegistry((controller: AbortController) =>
33
+ releaseRequestSignal(controller),
34
+ )
35
+ : null;
36
+
37
+ // Module-scope factory so the cleanup closure captures exactly the signal and
38
+ // the listener - built at the `fetchWithTimeout` call site it would share that
39
+ // scope's context and retain the request body for as long as the cleanup is
40
+ // held (same reason `_makeAbort` exists in client.ts).
41
+ function makeCleanup(signal: AbortSignal, listener: () => void): () => void {
42
+ return () => signal.removeEventListener('abort', listener);
43
+ }
44
+
45
+ export function registerRequestSignalCleanup(
46
+ controller: AbortController,
47
+ signal: AbortSignal,
48
+ listener: () => void,
49
+ ): void {
50
+ cleanups.set(controller, makeCleanup(signal, listener));
51
+ }
52
+
53
+ // The registered target is the response BODY, not the Response: a caller can
54
+ // keep a reader on the body and drop the Response wrapper (`.asResponse()`),
55
+ // and the listener must survive for as long as anything can still read - the
56
+ // body is what a live read keeps alive.
57
+ export function armAbandonmentBackstop(body: object, controller: AbortController): void {
58
+ if (cleanups.has(controller)) registry?.register(body, controller, controller);
59
+ }
60
+
61
+ export function releaseRequestSignal(controller: AbortController): void {
62
+ const cleanup = cleanups.get(controller);
63
+ if (cleanup) {
64
+ cleanups.delete(controller);
65
+ registry?.unregister(controller);
66
+ cleanup();
67
+ }
68
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Single source of truth for the `x-stainless-helper` telemetry header — the
3
+ * key, the closed value vocabulary, and per-object helper tagging. The
4
+ * append-don't-clobber merge for the header itself lives in
5
+ * {@link import('../internal/headers').buildHeaders} via `APPEND_HEADERS`.
6
+ */
7
+
8
+ /**
9
+ * Telemetry header naming the SDK helper(s) a request came from. Always this
10
+ * lowercase form; `buildHeaders` matches it case-insensitively for its append
11
+ * semantics, but a single canonical casing keeps every call site greppable.
12
+ */
13
+ export const STAINLESS_HELPER_HEADER = 'x-stainless-helper';
14
+
15
+ /** Telemetry header naming the SDK method (e.g. `stream`) in use. */
16
+ export const STAINLESS_HELPER_METHOD_HEADER = 'x-stainless-helper-method';
17
+
18
+ /**
19
+ * The closed set of helper telemetry tags, shared verbatim across SDKs. A
20
+ * typo at any call site is a type error rather than silently mistagged
21
+ * telemetry. Existing values keep their original spellings — telemetry
22
+ * consumers match on them, so renames lose history. New tags are hyphenated
23
+ * lowercase.
24
+ */
25
+ export type StainlessHelperHeaderValue =
26
+ | 'BetaToolRunner'
27
+ | 'betaZodTool'
28
+ | 'compaction'
29
+ | 'environments-work-poller'
30
+ | 'environments-worker'
31
+ | 'fallback-refusal-middleware'
32
+ | 'mcpContent'
33
+ | 'mcpMessage'
34
+ | 'mcpResourceToContent'
35
+ | 'mcpResourceToFile'
36
+ | 'mcpTool'
37
+ | 'session-tool-runner';
38
+
39
+ /**
40
+ * The `{ 'x-stainless-helper': value }` header dict, for passing into
41
+ * `buildHeaders` (which comma-appends `x-stainless-helper` across sources)
42
+ * or as `defaultHeaders`/per-request `headers`.
43
+ */
44
+ export function helperHeader(value: StainlessHelperHeaderValue): { [STAINLESS_HELPER_HEADER]: string } {
45
+ return { [STAINLESS_HELPER_HEADER]: value };
46
+ }
47
+
48
+ /**
49
+ * Symbol used to mark objects created by SDK helpers for tracking.
50
+ * The value is the helper name (e.g., 'mcpTool', 'betaZodTool').
51
+ */
52
+ export const SDK_HELPER_SYMBOL = Symbol('anthropic.sdk.stainlessHelper');
53
+
54
+ type StainlessHelperObject = { [SDK_HELPER_SYMBOL]: string };
55
+
56
+ export function wasCreatedByStainlessHelper(value: unknown): value is StainlessHelperObject {
57
+ return typeof value === 'object' && value !== null && SDK_HELPER_SYMBOL in value;
58
+ }
59
+
60
+ /**
61
+ * Collects helper names from tools and messages arrays.
62
+ * Returns a deduplicated array of helper names found.
63
+ */
64
+ export function collectStainlessHelpers(
65
+ tools: readonly unknown[] | undefined,
66
+ messages: readonly unknown[] | undefined,
67
+ ): string[] {
68
+ const helpers = new Set<string>();
69
+
70
+ // Collect from tools
71
+ if (tools) {
72
+ for (const tool of tools) {
73
+ if (wasCreatedByStainlessHelper(tool)) {
74
+ helpers.add(tool[SDK_HELPER_SYMBOL]);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Collect from messages and their content blocks
80
+ if (messages) {
81
+ for (const message of messages) {
82
+ if (wasCreatedByStainlessHelper(message)) {
83
+ helpers.add(message[SDK_HELPER_SYMBOL]);
84
+ }
85
+
86
+ const content = (message as { content?: unknown }).content;
87
+ if (Array.isArray(content)) {
88
+ for (const block of content) {
89
+ if (wasCreatedByStainlessHelper(block)) {
90
+ helpers.add(block[SDK_HELPER_SYMBOL]);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ return Array.from(helpers);
98
+ }
99
+
100
+ /**
101
+ * Builds x-stainless-helper header value from tools and messages.
102
+ * Returns an empty object if no helpers are found.
103
+ */
104
+ export function stainlessHelperHeader(
105
+ tools: readonly unknown[] | undefined,
106
+ messages: readonly unknown[] | undefined,
107
+ ): { 'x-stainless-helper'?: string } {
108
+ const helpers = collectStainlessHelpers(tools, messages);
109
+ if (helpers.length === 0) return {};
110
+ return { [STAINLESS_HELPER_HEADER]: helpers.join(', ') };
111
+ }
112
+
113
+ /**
114
+ * Builds x-stainless-helper header value from a file object.
115
+ * Returns an empty object if the file is not marked with a helper.
116
+ */
117
+ export function stainlessHelperHeaderFromFile(file: unknown): { 'x-stainless-helper'?: string } {
118
+ if (wasCreatedByStainlessHelper(file)) {
119
+ return { [STAINLESS_HELPER_HEADER]: file[SDK_HELPER_SYMBOL] };
120
+ }
121
+ return {};
122
+ }