@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.
- package/dist/action.d.ts +69 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +119 -0
- package/dist/action.js.map +1 -0
- package/dist/agent-definition.d.ts +6 -0
- package/dist/agent-definition.d.ts.map +1 -1
- package/dist/agent-definition.js.map +1 -1
- package/dist/agent.d.ts +2 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +35 -1
- package/dist/agent.js.map +1 -1
- package/dist/attachment-store-contract.d.ts +34 -0
- package/dist/attachment-store-contract.d.ts.map +1 -0
- package/dist/attachment-store-contract.js +0 -0
- package/dist/attachment-store-contract.js.map +1 -0
- package/dist/attachment-store.d.ts +107 -0
- package/dist/attachment-store.d.ts.map +1 -0
- package/dist/attachment-store.js +164 -0
- package/dist/attachment-store.js.map +1 -0
- package/dist/channel.d.ts +84 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +115 -0
- package/dist/channel.js.map +1 -0
- package/dist/context-budget.d.ts +16 -0
- package/dist/context-budget.d.ts.map +1 -1
- package/dist/context-budget.js +38 -3
- package/dist/context-budget.js.map +1 -1
- package/dist/conversation-projection.d.ts +52 -0
- package/dist/conversation-projection.d.ts.map +1 -0
- package/dist/conversation-projection.js +207 -0
- package/dist/conversation-projection.js.map +1 -0
- package/dist/conversation-stream-contract.d.ts +37 -0
- package/dist/conversation-stream-contract.d.ts.map +1 -0
- package/dist/conversation-stream-contract.js +453 -0
- package/dist/conversation-stream-contract.js.map +1 -0
- package/dist/conversation-stream.d.ts +141 -0
- package/dist/conversation-stream.d.ts.map +1 -0
- package/dist/conversation-stream.js +159 -0
- package/dist/conversation-stream.js.map +1 -0
- package/dist/cost-budget.d.ts +106 -1
- package/dist/cost-budget.d.ts.map +1 -1
- package/dist/cost-budget.js +48 -2
- package/dist/cost-budget.js.map +1 -1
- package/dist/delivered-message.d.ts +90 -0
- package/dist/delivered-message.d.ts.map +1 -0
- package/dist/delivered-message.js +154 -0
- package/dist/delivered-message.js.map +1 -0
- package/dist/dispatch.d.ts +108 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +179 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/error.d.ts +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/history.d.ts +49 -2
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +163 -0
- package/dist/history.js.map +1 -1
- package/dist/in-memory-submission-store.d.ts +53 -0
- package/dist/in-memory-submission-store.d.ts.map +1 -0
- package/dist/in-memory-submission-store.js +340 -0
- package/dist/in-memory-submission-store.js.map +1 -0
- package/dist/index.d.ts +51 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/dist/loop.d.ts +29 -3
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +167 -24
- package/dist/loop.js.map +1 -1
- package/dist/model.d.ts +37 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +250 -79
- package/dist/model.js.map +1 -1
- package/dist/otel-observer.d.ts +44 -0
- package/dist/otel-observer.d.ts.map +1 -0
- package/dist/otel-observer.js +165 -0
- package/dist/otel-observer.js.map +1 -0
- package/dist/persistence.d.ts +70 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +17 -0
- package/dist/persistence.js.map +1 -0
- package/dist/persistent-agent.d.ts +86 -0
- package/dist/persistent-agent.d.ts.map +1 -0
- package/dist/persistent-agent.js +75 -0
- package/dist/persistent-agent.js.map +1 -0
- package/dist/pi-loop-runtime.d.ts.map +1 -1
- package/dist/pi-loop-runtime.js +7 -1
- package/dist/pi-loop-runtime.js.map +1 -1
- package/dist/profile.d.ts +33 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +10 -0
- package/dist/profile.js.map +1 -0
- package/dist/providers.d.ts +14 -0
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +18 -1
- package/dist/providers.js.map +1 -1
- package/dist/result.d.ts +12 -0
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +26 -0
- package/dist/result.js.map +1 -1
- package/dist/retrieval.d.ts +61 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +26 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-mode.d.ts +1 -1
- package/dist/runtime-mode.d.ts.map +1 -1
- package/dist/runtime-mode.js +2 -1
- package/dist/runtime-mode.js.map +1 -1
- package/dist/scope-key.d.ts +25 -0
- package/dist/scope-key.d.ts.map +1 -0
- package/dist/scope-key.js +34 -0
- package/dist/scope-key.js.map +1 -0
- package/dist/session.d.ts +47 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +364 -35
- package/dist/session.js.map +1 -1
- package/dist/skills.d.ts +33 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +65 -0
- package/dist/skills.js.map +1 -0
- package/dist/store.d.ts +36 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +83 -0
- package/dist/store.js.map +1 -1
- package/dist/stream-chunks.d.ts +42 -0
- package/dist/stream-chunks.d.ts.map +1 -0
- package/dist/stream-chunks.js +138 -0
- package/dist/stream-chunks.js.map +1 -0
- package/dist/strict.d.ts +40 -11
- package/dist/strict.d.ts.map +1 -1
- package/dist/strict.js +22 -7
- package/dist/strict.js.map +1 -1
- package/dist/submission-context.d.ts +26 -0
- package/dist/submission-context.d.ts.map +1 -0
- package/dist/submission-context.js +20 -0
- package/dist/submission-context.js.map +1 -0
- package/dist/submission-runner.d.ts +150 -0
- package/dist/submission-runner.d.ts.map +1 -0
- package/dist/submission-runner.js +767 -0
- package/dist/submission-runner.js.map +1 -0
- package/dist/submission-state.d.ts +50 -0
- package/dist/submission-state.d.ts.map +1 -0
- package/dist/submission-state.js +82 -0
- package/dist/submission-state.js.map +1 -0
- package/dist/submission-store-contract.d.ts +31 -0
- package/dist/submission-store-contract.d.ts.map +1 -0
- package/dist/submission-store-contract.js +730 -0
- package/dist/submission-store-contract.js.map +1 -0
- package/dist/submission-store.d.ts +365 -0
- package/dist/submission-store.d.ts.map +1 -0
- package/dist/submission-store.js +191 -0
- package/dist/submission-store.js.map +1 -0
- package/dist/submission-telemetry.d.ts +56 -0
- package/dist/submission-telemetry.d.ts.map +1 -0
- package/dist/submission-telemetry.js +36 -0
- package/dist/submission-telemetry.js.map +1 -0
- package/dist/testing-contracts.d.ts +17 -0
- package/dist/testing-contracts.d.ts.map +1 -0
- package/dist/testing-contracts.js +14 -0
- package/dist/testing-contracts.js.map +1 -0
- package/dist/tools.d.ts +32 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +132 -6
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +132 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- 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 ??
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
options.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
error
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 ${
|
|
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 ${
|
|
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:
|
|
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:
|
|
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
|
|
603
|
-
|
|
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
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (!
|
|
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 (
|
|
883
|
-
const payload =
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (!
|
|
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 (
|
|
920
|
-
pendingEvent =
|
|
1090
|
+
if (trimmed.startsWith('event:')) {
|
|
1091
|
+
pendingEvent = trimmed.slice(6).trim();
|
|
921
1092
|
continue;
|
|
922
1093
|
}
|
|
923
|
-
if (
|
|
924
|
-
const payload =
|
|
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);
|