@fabric-harness/sdk 1.13.0 → 2.0.0

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 (169) hide show
  1. package/dist/action.d.ts +69 -0
  2. package/dist/action.d.ts.map +1 -0
  3. package/dist/action.js +119 -0
  4. package/dist/action.js.map +1 -0
  5. package/dist/agent-definition.d.ts +6 -0
  6. package/dist/agent-definition.d.ts.map +1 -1
  7. package/dist/agent-definition.js.map +1 -1
  8. package/dist/agent.d.ts +2 -0
  9. package/dist/agent.d.ts.map +1 -1
  10. package/dist/agent.js +35 -1
  11. package/dist/agent.js.map +1 -1
  12. package/dist/attachment-store-contract.d.ts +34 -0
  13. package/dist/attachment-store-contract.d.ts.map +1 -0
  14. package/dist/attachment-store-contract.js +0 -0
  15. package/dist/attachment-store-contract.js.map +1 -0
  16. package/dist/attachment-store.d.ts +107 -0
  17. package/dist/attachment-store.d.ts.map +1 -0
  18. package/dist/attachment-store.js +164 -0
  19. package/dist/attachment-store.js.map +1 -0
  20. package/dist/channel.d.ts +84 -0
  21. package/dist/channel.d.ts.map +1 -0
  22. package/dist/channel.js +115 -0
  23. package/dist/channel.js.map +1 -0
  24. package/dist/context-budget.d.ts +16 -0
  25. package/dist/context-budget.d.ts.map +1 -1
  26. package/dist/context-budget.js +38 -3
  27. package/dist/context-budget.js.map +1 -1
  28. package/dist/conversation-projection.d.ts +52 -0
  29. package/dist/conversation-projection.d.ts.map +1 -0
  30. package/dist/conversation-projection.js +207 -0
  31. package/dist/conversation-projection.js.map +1 -0
  32. package/dist/conversation-stream-contract.d.ts +37 -0
  33. package/dist/conversation-stream-contract.d.ts.map +1 -0
  34. package/dist/conversation-stream-contract.js +453 -0
  35. package/dist/conversation-stream-contract.js.map +1 -0
  36. package/dist/conversation-stream.d.ts +141 -0
  37. package/dist/conversation-stream.d.ts.map +1 -0
  38. package/dist/conversation-stream.js +159 -0
  39. package/dist/conversation-stream.js.map +1 -0
  40. package/dist/cost-budget.d.ts +106 -1
  41. package/dist/cost-budget.d.ts.map +1 -1
  42. package/dist/cost-budget.js +48 -2
  43. package/dist/cost-budget.js.map +1 -1
  44. package/dist/delivered-message.d.ts +90 -0
  45. package/dist/delivered-message.d.ts.map +1 -0
  46. package/dist/delivered-message.js +154 -0
  47. package/dist/delivered-message.js.map +1 -0
  48. package/dist/dispatch.d.ts +108 -0
  49. package/dist/dispatch.d.ts.map +1 -0
  50. package/dist/dispatch.js +179 -0
  51. package/dist/dispatch.js.map +1 -0
  52. package/dist/error.d.ts +1 -1
  53. package/dist/error.d.ts.map +1 -1
  54. package/dist/error.js.map +1 -1
  55. package/dist/history.d.ts +49 -2
  56. package/dist/history.d.ts.map +1 -1
  57. package/dist/history.js +163 -0
  58. package/dist/history.js.map +1 -1
  59. package/dist/in-memory-submission-store.d.ts +53 -0
  60. package/dist/in-memory-submission-store.d.ts.map +1 -0
  61. package/dist/in-memory-submission-store.js +340 -0
  62. package/dist/in-memory-submission-store.js.map +1 -0
  63. package/dist/index.d.ts +51 -13
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +28 -7
  66. package/dist/index.js.map +1 -1
  67. package/dist/loop.d.ts +29 -3
  68. package/dist/loop.d.ts.map +1 -1
  69. package/dist/loop.js +167 -24
  70. package/dist/loop.js.map +1 -1
  71. package/dist/model.d.ts +37 -0
  72. package/dist/model.d.ts.map +1 -1
  73. package/dist/model.js +250 -79
  74. package/dist/model.js.map +1 -1
  75. package/dist/otel-observer.d.ts +44 -0
  76. package/dist/otel-observer.d.ts.map +1 -0
  77. package/dist/otel-observer.js +165 -0
  78. package/dist/otel-observer.js.map +1 -0
  79. package/dist/persistence.d.ts +70 -0
  80. package/dist/persistence.d.ts.map +1 -0
  81. package/dist/persistence.js +17 -0
  82. package/dist/persistence.js.map +1 -0
  83. package/dist/persistent-agent.d.ts +86 -0
  84. package/dist/persistent-agent.d.ts.map +1 -0
  85. package/dist/persistent-agent.js +75 -0
  86. package/dist/persistent-agent.js.map +1 -0
  87. package/dist/pi-loop-runtime.d.ts.map +1 -1
  88. package/dist/pi-loop-runtime.js +7 -1
  89. package/dist/pi-loop-runtime.js.map +1 -1
  90. package/dist/profile.d.ts +33 -0
  91. package/dist/profile.d.ts.map +1 -0
  92. package/dist/profile.js +10 -0
  93. package/dist/profile.js.map +1 -0
  94. package/dist/providers.d.ts +14 -0
  95. package/dist/providers.d.ts.map +1 -1
  96. package/dist/providers.js +18 -1
  97. package/dist/providers.js.map +1 -1
  98. package/dist/result.d.ts +12 -0
  99. package/dist/result.d.ts.map +1 -1
  100. package/dist/result.js +26 -0
  101. package/dist/result.js.map +1 -1
  102. package/dist/retrieval.d.ts +61 -0
  103. package/dist/retrieval.d.ts.map +1 -0
  104. package/dist/retrieval.js +26 -0
  105. package/dist/retrieval.js.map +1 -0
  106. package/dist/runtime-mode.d.ts +1 -1
  107. package/dist/runtime-mode.d.ts.map +1 -1
  108. package/dist/runtime-mode.js +2 -1
  109. package/dist/runtime-mode.js.map +1 -1
  110. package/dist/scope-key.d.ts +25 -0
  111. package/dist/scope-key.d.ts.map +1 -0
  112. package/dist/scope-key.js +34 -0
  113. package/dist/scope-key.js.map +1 -0
  114. package/dist/session.d.ts +47 -1
  115. package/dist/session.d.ts.map +1 -1
  116. package/dist/session.js +364 -35
  117. package/dist/session.js.map +1 -1
  118. package/dist/skills.d.ts +33 -0
  119. package/dist/skills.d.ts.map +1 -0
  120. package/dist/skills.js +65 -0
  121. package/dist/skills.js.map +1 -0
  122. package/dist/store.d.ts +36 -1
  123. package/dist/store.d.ts.map +1 -1
  124. package/dist/store.js +83 -0
  125. package/dist/store.js.map +1 -1
  126. package/dist/stream-chunks.d.ts +42 -0
  127. package/dist/stream-chunks.d.ts.map +1 -0
  128. package/dist/stream-chunks.js +138 -0
  129. package/dist/stream-chunks.js.map +1 -0
  130. package/dist/strict.d.ts +40 -11
  131. package/dist/strict.d.ts.map +1 -1
  132. package/dist/strict.js +22 -7
  133. package/dist/strict.js.map +1 -1
  134. package/dist/submission-context.d.ts +26 -0
  135. package/dist/submission-context.d.ts.map +1 -0
  136. package/dist/submission-context.js +20 -0
  137. package/dist/submission-context.js.map +1 -0
  138. package/dist/submission-runner.d.ts +150 -0
  139. package/dist/submission-runner.d.ts.map +1 -0
  140. package/dist/submission-runner.js +767 -0
  141. package/dist/submission-runner.js.map +1 -0
  142. package/dist/submission-state.d.ts +50 -0
  143. package/dist/submission-state.d.ts.map +1 -0
  144. package/dist/submission-state.js +82 -0
  145. package/dist/submission-state.js.map +1 -0
  146. package/dist/submission-store-contract.d.ts +31 -0
  147. package/dist/submission-store-contract.d.ts.map +1 -0
  148. package/dist/submission-store-contract.js +730 -0
  149. package/dist/submission-store-contract.js.map +1 -0
  150. package/dist/submission-store.d.ts +365 -0
  151. package/dist/submission-store.d.ts.map +1 -0
  152. package/dist/submission-store.js +191 -0
  153. package/dist/submission-store.js.map +1 -0
  154. package/dist/submission-telemetry.d.ts +56 -0
  155. package/dist/submission-telemetry.d.ts.map +1 -0
  156. package/dist/submission-telemetry.js +36 -0
  157. package/dist/submission-telemetry.js.map +1 -0
  158. package/dist/testing-contracts.d.ts +17 -0
  159. package/dist/testing-contracts.d.ts.map +1 -0
  160. package/dist/testing-contracts.js +14 -0
  161. package/dist/testing-contracts.js.map +1 -0
  162. package/dist/tools.d.ts +32 -4
  163. package/dist/tools.d.ts.map +1 -1
  164. package/dist/tools.js +132 -6
  165. package/dist/tools.js.map +1 -1
  166. package/dist/types.d.ts +132 -3
  167. package/dist/types.d.ts.map +1 -1
  168. package/dist/types.js.map +1 -1
  169. package/package.json +16 -8
package/dist/model.js CHANGED
@@ -7,61 +7,78 @@ export function toolsToModelSchemas(tools) {
7
7
  ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
8
8
  }));
9
9
  }
10
+ const MAX_TRANSIENT_MODEL_RETRIES = 3;
11
+ const TRANSIENT_MODEL_RETRY_BASE_DELAY_MS = 2_000;
10
12
  export async function generateWithRuntime(provider, request, options = {}) {
11
- const maxAttempts = (options.retries ?? 0) + 1;
13
+ const maxAttempts = (options.retries ?? MAX_TRANSIENT_MODEL_RETRIES) + 1;
14
+ const baseDelayMs = options.retryDelayMs ?? TRANSIENT_MODEL_RETRY_BASE_DELAY_MS;
12
15
  let lastError;
13
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
14
- const startedAt = Date.now();
15
- const controller = new AbortController();
16
- const timeout = options.timeoutMs
17
- ? setTimeout(() => controller.abort(new Error(`Model call timed out after ${options.timeoutMs}ms`)), options.timeoutMs)
18
- : undefined;
19
- const abortListener = () => controller.abort(options.signal?.reason);
20
- options.signal?.addEventListener('abort', abortListener, { once: true });
21
- await options.onAttempt?.({ provider: provider.name, attempt, maxAttempts, status: 'start', ...(request.model !== undefined ? { model: request.model } : {}) });
22
- try {
23
- const response = await provider.generate({ ...request, signal: controller.signal });
24
- if (timeout)
25
- clearTimeout(timeout);
26
- options.signal?.removeEventListener('abort', abortListener);
27
- await options.onAttempt?.({
28
- provider: provider.name,
29
- attempt,
30
- maxAttempts,
31
- status: 'success',
32
- durationMs: Date.now() - startedAt,
33
- ...(request.model !== undefined ? { model: request.model } : {}),
34
- ...(response.usage !== undefined ? { usage: response.usage } : {}),
35
- });
36
- return response;
37
- }
38
- catch (error) {
39
- if (timeout)
40
- clearTimeout(timeout);
41
- options.signal?.removeEventListener('abort', abortListener);
42
- lastError = error;
43
- const message = error instanceof Error ? error.message : String(error);
44
- const canRetry = attempt < maxAttempts && !options.signal?.aborted && isRetryableModelError(error);
45
- await options.onAttempt?.({
46
- provider: provider.name,
47
- attempt,
48
- maxAttempts,
49
- status: canRetry ? 'retry' : 'error',
50
- durationMs: Date.now() - startedAt,
51
- ...(request.model !== undefined ? { model: request.model } : {}),
52
- error: message,
53
- });
54
- if (!canRetry)
55
- break;
56
- // Honor Retry-After when the provider supplied one — saves wasted
57
- // retries during quota crunches. Falls back to exponential backoff.
58
- const retryAfterMs = error instanceof ProviderHttpError ? error.retryAfterMs : undefined;
59
- const waitMs = retryAfterMs && retryAfterMs > 0
60
- ? Math.min(retryAfterMs, 60_000)
61
- : retryDelay(options.retryDelayMs ?? 250, attempt);
62
- await delay(waitMs);
16
+ // Dedicated abort controller for retry sleeps separate from the
17
+ // per-attempt timeout controller so that session.abort() can cancel
18
+ // a pending backoff without tearing down an in-flight model call.
19
+ const retryAbortController = new AbortController();
20
+ const onCallerAbort = () => retryAbortController.abort(options.signal?.reason);
21
+ options.signal?.addEventListener('abort', onCallerAbort, { once: true });
22
+ try {
23
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
24
+ if (options.signal?.aborted) {
25
+ throw createAbortError(options.signal.reason);
26
+ }
27
+ const startedAt = Date.now();
28
+ const controller = new AbortController();
29
+ const timeout = options.timeoutMs
30
+ ? setTimeout(() => controller.abort(new Error(`Model call timed out after ${options.timeoutMs}ms`)), options.timeoutMs)
31
+ : undefined;
32
+ const abortListener = () => controller.abort(options.signal?.reason);
33
+ options.signal?.addEventListener('abort', abortListener, { once: true });
34
+ await options.onAttempt?.({ provider: provider.name, attempt, maxAttempts, status: 'start', ...(request.model !== undefined ? { model: request.model } : {}) });
35
+ try {
36
+ const response = await provider.generate({ ...request, signal: controller.signal });
37
+ if (timeout)
38
+ clearTimeout(timeout);
39
+ options.signal?.removeEventListener('abort', abortListener);
40
+ await options.onAttempt?.({
41
+ provider: provider.name,
42
+ attempt,
43
+ maxAttempts,
44
+ status: 'success',
45
+ durationMs: Date.now() - startedAt,
46
+ ...(request.model !== undefined ? { model: request.model } : {}),
47
+ ...(response.usage !== undefined ? { usage: response.usage } : {}),
48
+ });
49
+ return response;
50
+ }
51
+ catch (error) {
52
+ if (timeout)
53
+ clearTimeout(timeout);
54
+ options.signal?.removeEventListener('abort', abortListener);
55
+ lastError = error;
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ const canRetry = attempt < maxAttempts && !options.signal?.aborted && isRetryableModelError(error);
58
+ await options.onAttempt?.({
59
+ provider: provider.name,
60
+ attempt,
61
+ maxAttempts,
62
+ status: canRetry ? 'retry' : 'error',
63
+ durationMs: Date.now() - startedAt,
64
+ ...(request.model !== undefined ? { model: request.model } : {}),
65
+ error: message,
66
+ });
67
+ if (!canRetry)
68
+ break;
69
+ // Honor Retry-After when the provider supplied one — saves wasted
70
+ // retries during quota crunches. Falls back to exponential backoff.
71
+ const retryAfterMs = error instanceof ProviderHttpError ? error.retryAfterMs : undefined;
72
+ const waitMs = retryAfterMs && retryAfterMs > 0
73
+ ? Math.min(retryAfterMs, 60_000)
74
+ : calculateRetryDelay(baseDelayMs, attempt);
75
+ await sleepWithAbort(waitMs, retryAbortController.signal);
76
+ }
63
77
  }
64
78
  }
79
+ finally {
80
+ options.signal?.removeEventListener('abort', onCallerAbort);
81
+ }
65
82
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
66
83
  }
67
84
  export class MockModelProvider {
@@ -82,10 +99,78 @@ export class MockModelProvider {
82
99
  return { message: { role: 'assistant', content }, usage: estimateUsage([...request.messages, { role: 'assistant', content }]) };
83
100
  }
84
101
  }
102
+ // ─── Reasoning effort ────────────────────────────────────────────────────────
103
+ //
104
+ // Conservative capability detection for the native HTTP providers (the default
105
+ // pi-agent-core loop does its own per-model clamping). Reasoning is only ever
106
+ // applied when a caller explicitly sets `thinkingLevel`; a false negative here
107
+ // safely drops the level, while sending an unsupported control would 400. Keep
108
+ // the patterns tight and matched to well-established reasoning model families.
109
+ const REASONING_MODEL_PATTERNS = [
110
+ /^o\d/i, // OpenAI o-series: o1, o3, o4...
111
+ /^gpt-5/i, // OpenAI gpt-5 family
112
+ /gpt-oss/i, // OpenAI open-weights reasoning
113
+ /^claude-3-7/i, // Claude 3.7 extended thinking
114
+ /^claude-(opus|sonnet|haiku)-4/i, // Claude 4.x families
115
+ /^gemini-2\.5/i, // Gemini 2.5 thinking
116
+ ];
117
+ /** Whether a model id (with or without provider prefix) is a known reasoning model. */
118
+ export function modelSupportsReasoning(model) {
119
+ const id = model.includes('/') ? model.slice(model.lastIndexOf('/') + 1) : model;
120
+ return REASONING_MODEL_PATTERNS.some((re) => re.test(id));
121
+ }
122
+ /** True when a reasoning control should be applied for this model + requested level. */
123
+ function shouldApplyReasoning(model, level) {
124
+ return level !== undefined && level !== 'off' && modelSupportsReasoning(model);
125
+ }
126
+ /** OpenAI / Workers-AI style `reasoning_effort` value. */
127
+ function openAIReasoningEffort(level) {
128
+ switch (level) {
129
+ case 'minimal':
130
+ case 'low':
131
+ return 'low';
132
+ case 'medium':
133
+ return 'medium';
134
+ case 'high':
135
+ case 'xhigh':
136
+ return 'high';
137
+ }
138
+ }
139
+ /** Anthropic extended-thinking token budget (>= 1024; `max_tokens` must exceed it). */
140
+ function anthropicThinkingBudget(level) {
141
+ switch (level) {
142
+ case 'minimal':
143
+ return 1024;
144
+ case 'low':
145
+ return 2048;
146
+ case 'medium':
147
+ return 4096;
148
+ case 'high':
149
+ return 8192;
150
+ case 'xhigh':
151
+ return 16384;
152
+ }
153
+ }
154
+ /** Gemini `thinkingConfig.thinkingBudget` token count. */
155
+ function geminiThinkingBudget(level) {
156
+ switch (level) {
157
+ case 'minimal':
158
+ return 512;
159
+ case 'low':
160
+ return 1024;
161
+ case 'medium':
162
+ return 4096;
163
+ case 'high':
164
+ return 12288;
165
+ case 'xhigh':
166
+ return 24576;
167
+ }
168
+ }
85
169
  export class OpenAICompatibleModelProvider {
86
170
  name;
87
171
  baseUrl;
88
172
  apiKey;
173
+ tokenProvider;
89
174
  defaultModel;
90
175
  headers;
91
176
  rateLimiter;
@@ -94,30 +179,37 @@ export class OpenAICompatibleModelProvider {
94
179
  this.name = options.name ?? 'openai-compatible';
95
180
  this.baseUrl = options.baseUrl.replace(/\/$/, '');
96
181
  this.apiKey = options.apiKey;
182
+ this.tokenProvider = options.tokenProvider;
97
183
  if (options.defaultModel !== undefined)
98
184
  this.defaultModel = options.defaultModel;
99
185
  this.headers = options.headers ?? {};
100
186
  this.rateLimiter = options.rateLimiter;
101
187
  this.rateLimitKey = `${this.name}:${rateLimitKeyForApiKey(this.apiKey)}`;
102
188
  }
189
+ /** Resolves the bearer token for a request: the rotating `tokenProvider` if set, else `apiKey`. */
190
+ async authToken() {
191
+ return this.tokenProvider ? await this.tokenProvider() : this.apiKey;
192
+ }
103
193
  async generate(request) {
104
194
  const model = request.model ?? this.defaultModel;
105
195
  if (!model)
106
196
  throw new Error(`${this.name}: model is required.`);
107
197
  if (this.rateLimiter)
108
198
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
199
+ const token = await this.authToken();
109
200
  const init = {
110
201
  method: 'POST',
111
202
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
112
203
  headers: {
113
204
  'content-type': 'application/json',
114
- authorization: `Bearer ${this.apiKey}`,
205
+ authorization: `Bearer ${token}`,
115
206
  ...this.headers,
116
207
  },
117
208
  body: JSON.stringify({
118
209
  model,
119
210
  messages: request.messages.map(toOpenAIMessage),
120
211
  ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}),
212
+ ...(shouldApplyReasoning(model, request.thinkingLevel) ? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) } : {}),
121
213
  }),
122
214
  };
123
215
  const response = await fetch(`${this.baseUrl}/chat/completions`, init);
@@ -143,12 +235,13 @@ export class OpenAICompatibleModelProvider {
143
235
  throw new Error(`${this.name}: model is required.`);
144
236
  if (this.rateLimiter)
145
237
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
238
+ const token = await this.authToken();
146
239
  const init = {
147
240
  method: 'POST',
148
241
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
149
242
  headers: {
150
243
  'content-type': 'application/json',
151
- authorization: `Bearer ${this.apiKey}`,
244
+ authorization: `Bearer ${token}`,
152
245
  accept: 'text/event-stream',
153
246
  ...this.headers,
154
247
  },
@@ -158,6 +251,7 @@ export class OpenAICompatibleModelProvider {
158
251
  stream_options: { include_usage: true },
159
252
  messages: request.messages.map(toOpenAIMessage),
160
253
  ...(request.tools !== undefined ? { tools: request.tools.map(toOpenAITool) } : {}),
254
+ ...(shouldApplyReasoning(model, request.thinkingLevel) ? { reasoning_effort: openAIReasoningEffort(request.thinkingLevel) } : {}),
161
255
  }),
162
256
  };
163
257
  const response = await fetch(`${this.baseUrl}/chat/completions`, init);
@@ -283,7 +377,7 @@ export class GeminiModelProvider {
283
377
  method: 'POST',
284
378
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
285
379
  headers: { 'content-type': 'application/json', ...this.headers },
286
- body: JSON.stringify(toGeminiRequest(request)),
380
+ body: JSON.stringify(toGeminiRequest(request, model)),
287
381
  });
288
382
  if (!response.ok)
289
383
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
@@ -317,7 +411,7 @@ export class VertexAIModelProvider {
317
411
  method: 'POST',
318
412
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
319
413
  headers: { 'content-type': 'application/json', authorization: `Bearer ${this.accessToken}`, ...this.headers },
320
- body: JSON.stringify(toGeminiRequest(request)),
414
+ body: JSON.stringify(toGeminiRequest(request, model)),
321
415
  });
322
416
  if (!response.ok)
323
417
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
@@ -452,11 +546,13 @@ export class AnthropicModelProvider {
452
546
  if (this.rateLimiter)
453
547
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
454
548
  const { system, messages } = toAnthropicMessages(request.messages);
549
+ const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel) ? anthropicThinkingBudget(request.thinkingLevel) : undefined;
550
+ const maxTokens = thinkingBudget !== undefined ? Math.max(this.maxTokens, thinkingBudget + 1024) : this.maxTokens;
455
551
  const response = await fetch(`${this.baseUrl}/messages`, {
456
552
  method: 'POST',
457
553
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
458
554
  headers: { 'content-type': 'application/json', 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01', ...this.headers },
459
- body: JSON.stringify({ model, max_tokens: this.maxTokens, ...(system ? { system } : {}), messages, ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}) }),
555
+ body: JSON.stringify({ model, max_tokens: maxTokens, ...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}), ...(system ? { system } : {}), messages, ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}) }),
460
556
  });
461
557
  if (!response.ok)
462
558
  throw formatProviderHttpError(this.name, model, response.status, response.statusText, await response.text(), response.headers);
@@ -471,6 +567,8 @@ export class AnthropicModelProvider {
471
567
  if (this.rateLimiter)
472
568
  await this.rateLimiter.acquire(this.rateLimitKey, request.signal ? { signal: request.signal } : undefined);
473
569
  const { system, messages } = toAnthropicMessages(request.messages);
570
+ const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel) ? anthropicThinkingBudget(request.thinkingLevel) : undefined;
571
+ const maxTokens = thinkingBudget !== undefined ? Math.max(this.maxTokens, thinkingBudget + 1024) : this.maxTokens;
474
572
  const response = await fetch(`${this.baseUrl}/messages`, {
475
573
  method: 'POST',
476
574
  ...(request.signal !== undefined ? { signal: request.signal } : {}),
@@ -484,7 +582,8 @@ export class AnthropicModelProvider {
484
582
  body: JSON.stringify({
485
583
  model,
486
584
  stream: true,
487
- max_tokens: this.maxTokens,
585
+ max_tokens: maxTokens,
586
+ ...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}),
488
587
  ...(system ? { system } : {}),
489
588
  messages,
490
589
  ...(request.tools !== undefined ? { tools: request.tools.map(toAnthropicTool) } : {}),
@@ -599,19 +698,44 @@ function estimateUsage(messages) {
599
698
  const chars = messages.reduce((sum, message) => sum + message.content.length, 0);
600
699
  return { totalTokens: Math.ceil(chars / 4) };
601
700
  }
602
- function retryDelay(baseMs, attempt) {
603
- return Math.min(30_000, Math.round(baseMs * 2 ** Math.max(0, attempt - 1)));
701
+ function calculateRetryDelay(baseMs, attempt) {
702
+ const baseDelay = baseMs * 2 ** (attempt - 1);
703
+ const jittered = baseDelay * (0.75 + Math.random() * 0.5);
704
+ return Math.round(jittered);
604
705
  }
605
706
  function isRetryableModelError(error) {
606
707
  if (error instanceof ProviderHttpError)
607
708
  return error.status === 429 || error.status >= 500;
608
709
  if (error instanceof Error && error.name === 'AbortError')
609
710
  return false;
610
- return true;
711
+ const message = error instanceof Error ? error.message : String(error);
712
+ return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|network.?error|connection.?(?:reset|refused|lost)|socket hang up|fetch failed|timed? out|timeout|terminated/i.test(message);
611
713
  }
612
714
  function delay(ms) {
613
715
  return new Promise((resolve) => setTimeout(resolve, ms));
614
716
  }
717
+ function sleepWithAbort(ms, signal) {
718
+ if (signal.aborted)
719
+ return Promise.reject(createAbortError(signal.reason));
720
+ return new Promise((resolve, reject) => {
721
+ const timer = setTimeout(() => {
722
+ signal.removeEventListener('abort', onAbort);
723
+ resolve();
724
+ }, ms);
725
+ const onAbort = () => {
726
+ clearTimeout(timer);
727
+ signal.removeEventListener('abort', onAbort);
728
+ reject(createAbortError(signal.reason));
729
+ };
730
+ signal.addEventListener('abort', onAbort, { once: true });
731
+ });
732
+ }
733
+ function createAbortError(reason) {
734
+ const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : 'This operation was aborted';
735
+ const err = new Error(message);
736
+ err.name = 'AbortError';
737
+ return err;
738
+ }
615
739
  export function toOpenAIMessage(message) {
616
740
  if (message.role === 'tool') {
617
741
  return { role: 'tool', tool_call_id: message.toolCallId ?? message.name ?? 'tool', content: message.content };
@@ -701,7 +825,7 @@ function toAnthropicMessages(messages) {
701
825
  });
702
826
  return { ...(system ? { system } : {}), messages: converted };
703
827
  }
704
- function toGeminiRequest(request) {
828
+ function toGeminiRequest(request, model) {
705
829
  const system = request.messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n');
706
830
  const contents = request.messages.filter((message) => message.role !== 'system').map((message) => {
707
831
  if (message.role === 'tool')
@@ -710,10 +834,12 @@ function toGeminiRequest(request) {
710
834
  return { role: 'model', parts: message.toolCalls.map((call) => ({ functionCall: { name: call.name, args: call.input ?? {} } })) };
711
835
  return { role: message.role === 'assistant' ? 'model' : 'user', parts: [{ text: message.content }] };
712
836
  });
837
+ const thinkingBudget = shouldApplyReasoning(model, request.thinkingLevel) ? geminiThinkingBudget(request.thinkingLevel) : undefined;
713
838
  return {
714
839
  ...(system ? { systemInstruction: { parts: [{ text: system }] } } : {}),
715
840
  contents,
716
841
  ...(request.tools?.length ? { tools: [{ functionDeclarations: request.tools.map(toGeminiTool) }] } : {}),
842
+ ...(thinkingBudget !== undefined ? { generationConfig: { thinkingConfig: { thinkingBudget } } } : {}),
717
843
  };
718
844
  }
719
845
  function toGeminiTool(tool) {
@@ -758,6 +884,7 @@ function toBedrockRequest(request) {
758
884
  ...(system.length ? { system } : {}),
759
885
  messages,
760
886
  ...(request.tools?.length ? { toolConfig: { tools: request.tools.map(toBedrockTool) } } : {}),
887
+ ...(request.sessionId !== undefined ? { sessionId: request.sessionId } : {}),
761
888
  };
762
889
  }
763
890
  function toBedrockTool(tool) {
@@ -858,6 +985,48 @@ function safeJsonParse(value) {
858
985
  return undefined;
859
986
  }
860
987
  }
988
+ /**
989
+ * Split a string into lines handling LF (`\n`), CRLF (`\r\n`), and CR-only
990
+ * (`\r`) line endings. When a buffer ends with `\r` and the next chunk starts
991
+ * with `\n`, the pair is treated as a single CRLF and no empty line is emitted.
992
+ *
993
+ * @param buffer The text accumulated so far.
994
+ * @param pendingCR `true` when the previous chunk ended with a bare `\r`.
995
+ * @returns Lines found in the buffer, the updated `pendingCR` flag, and the
996
+ * unprocessed remainder to carry into the next chunk.
997
+ */
998
+ function splitLines(buffer, pendingCR) {
999
+ const lines = [];
1000
+ let start = 0;
1001
+ let pending = false;
1002
+ // If previous chunk ended with \r and this one starts with \n, skip the \n
1003
+ // as it completes a split CRLF sequence.
1004
+ if (pendingCR && buffer.length > 0 && buffer[0] === '\n') {
1005
+ start = 1;
1006
+ }
1007
+ for (let i = start; i < buffer.length; i++) {
1008
+ const ch = buffer[i];
1009
+ if (ch === '\n') {
1010
+ lines.push(buffer.slice(start, i));
1011
+ start = i + 1;
1012
+ }
1013
+ else if (ch === '\r') {
1014
+ if (i + 1 < buffer.length && buffer[i + 1] === '\n') {
1015
+ // CRLF within the same buffer
1016
+ lines.push(buffer.slice(start, i));
1017
+ i++; // skip the \n
1018
+ start = i + 1;
1019
+ }
1020
+ else {
1021
+ // CR-only or the first half of a CRLF split across chunks
1022
+ lines.push(buffer.slice(start, i));
1023
+ pending = true;
1024
+ start = i + 1;
1025
+ }
1026
+ }
1027
+ }
1028
+ return { lines, pendingCR: pending, remainder: buffer.slice(start) };
1029
+ }
861
1030
  /**
862
1031
  * Decode an OpenAI-style SSE stream — yields the JSON payload from each
863
1032
  * `data: ...` line. Yields the literal sentinel `[DONE]` when the stream ends.
@@ -866,21 +1035,22 @@ async function* decodeOpenAISSE(stream) {
866
1035
  const decoder = new TextDecoder();
867
1036
  const reader = stream.getReader();
868
1037
  let buffer = '';
1038
+ let pendingCR = false;
869
1039
  try {
870
1040
  while (true) {
871
1041
  const { value, done } = await reader.read();
872
1042
  if (done)
873
1043
  break;
874
1044
  buffer += decoder.decode(value, { stream: true });
875
- let nl = buffer.indexOf('\n');
876
- while (nl >= 0) {
877
- const line = buffer.slice(0, nl).trim();
878
- buffer = buffer.slice(nl + 1);
879
- nl = buffer.indexOf('\n');
880
- if (!line || line.startsWith(':'))
1045
+ const { lines, pendingCR: newPendingCR, remainder } = splitLines(buffer, pendingCR);
1046
+ pendingCR = newPendingCR;
1047
+ buffer = remainder;
1048
+ for (const line of lines) {
1049
+ const trimmed = line.trim();
1050
+ if (!trimmed || trimmed.startsWith(':'))
881
1051
  continue;
882
- if (line.startsWith('data:')) {
883
- const payload = line.slice(5).trim();
1052
+ if (trimmed.startsWith('data:')) {
1053
+ const payload = trimmed.slice(5).trim();
884
1054
  if (!payload)
885
1055
  continue;
886
1056
  yield payload;
@@ -901,27 +1071,28 @@ async function* decodeAnthropicSSE(stream) {
901
1071
  const reader = stream.getReader();
902
1072
  let buffer = '';
903
1073
  let pendingEvent;
1074
+ let pendingCR = false;
904
1075
  try {
905
1076
  while (true) {
906
1077
  const { value, done } = await reader.read();
907
1078
  if (done)
908
1079
  break;
909
1080
  buffer += decoder.decode(value, { stream: true });
910
- let nl = buffer.indexOf('\n');
911
- while (nl >= 0) {
912
- const line = buffer.slice(0, nl).trim();
913
- buffer = buffer.slice(nl + 1);
914
- nl = buffer.indexOf('\n');
915
- if (!line) {
1081
+ const { lines, pendingCR: newPendingCR, remainder } = splitLines(buffer, pendingCR);
1082
+ pendingCR = newPendingCR;
1083
+ buffer = remainder;
1084
+ for (const line of lines) {
1085
+ const trimmed = line.trim();
1086
+ if (!trimmed) {
916
1087
  pendingEvent = undefined;
917
1088
  continue;
918
1089
  }
919
- if (line.startsWith('event:')) {
920
- pendingEvent = line.slice(6).trim();
1090
+ if (trimmed.startsWith('event:')) {
1091
+ pendingEvent = trimmed.slice(6).trim();
921
1092
  continue;
922
1093
  }
923
- if (line.startsWith('data:')) {
924
- const payload = line.slice(5).trim();
1094
+ if (trimmed.startsWith('data:')) {
1095
+ const payload = trimmed.slice(5).trim();
925
1096
  if (!payload)
926
1097
  continue;
927
1098
  const parsed = safeJsonParse(payload);