@framers/agentos 0.12.18 → 0.12.20

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 (44) hide show
  1. package/dist/api/model.d.ts.map +1 -1
  2. package/dist/api/model.js +2 -0
  3. package/dist/api/model.js.map +1 -1
  4. package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
  5. package/dist/api/runtime/provider-defaults.js +5 -0
  6. package/dist/api/runtime/provider-defaults.js.map +1 -1
  7. package/dist/core/config/AgentOSConfig.d.ts +1 -0
  8. package/dist/core/config/AgentOSConfig.d.ts.map +1 -1
  9. package/dist/core/config/AgentOSConfig.js +18 -1
  10. package/dist/core/config/AgentOSConfig.js.map +1 -1
  11. package/dist/core/llm/providers/AIModelProviderManager.d.ts +2 -1
  12. package/dist/core/llm/providers/AIModelProviderManager.d.ts.map +1 -1
  13. package/dist/core/llm/providers/AIModelProviderManager.js +4 -0
  14. package/dist/core/llm/providers/AIModelProviderManager.js.map +1 -1
  15. package/dist/core/llm/providers/errors/RequestyProviderError.d.ts +39 -0
  16. package/dist/core/llm/providers/errors/RequestyProviderError.d.ts.map +1 -0
  17. package/dist/core/llm/providers/errors/RequestyProviderError.js +42 -0
  18. package/dist/core/llm/providers/errors/RequestyProviderError.js.map +1 -0
  19. package/dist/core/llm/providers/implementations/RequestyProvider.d.ts +64 -0
  20. package/dist/core/llm/providers/implementations/RequestyProvider.d.ts.map +1 -0
  21. package/dist/core/llm/providers/implementations/RequestyProvider.js +652 -0
  22. package/dist/core/llm/providers/implementations/RequestyProvider.js.map +1 -0
  23. package/dist/core/llm/providers/structuredOutputFormat.d.ts +1 -0
  24. package/dist/core/llm/providers/structuredOutputFormat.d.ts.map +1 -1
  25. package/dist/core/llm/providers/structuredOutputFormat.js +2 -1
  26. package/dist/core/llm/providers/structuredOutputFormat.js.map +1 -1
  27. package/dist/io/speech/SpeechProviderResolver.d.ts.map +1 -1
  28. package/dist/io/speech/SpeechProviderResolver.js +1 -0
  29. package/dist/io/speech/SpeechProviderResolver.js.map +1 -1
  30. package/dist/io/speech/SpeechRuntime.d.ts.map +1 -1
  31. package/dist/io/speech/SpeechRuntime.js +12 -0
  32. package/dist/io/speech/SpeechRuntime.js.map +1 -1
  33. package/dist/io/speech/index.d.ts +1 -0
  34. package/dist/io/speech/index.d.ts.map +1 -1
  35. package/dist/io/speech/index.js +1 -0
  36. package/dist/io/speech/index.js.map +1 -1
  37. package/dist/io/speech/providerCatalog.d.ts.map +1 -1
  38. package/dist/io/speech/providerCatalog.js +11 -0
  39. package/dist/io/speech/providerCatalog.js.map +1 -1
  40. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.d.ts +58 -0
  41. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.d.ts.map +1 -0
  42. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.js +209 -0
  43. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.js.map +1 -0
  44. package/package.json +1 -1
@@ -0,0 +1,652 @@
1
+ // File: backend/agentos/core/llm/providers/implementations/RequestyProvider.ts
2
+ /**
3
+ * @fileoverview Implements the IProvider interface for Requesty, an OpenAI-compatible
4
+ * LLM gateway that provides access to a wide variety of LLMs from different providers
5
+ * through a unified API. This provider handles routing requests to the specified models
6
+ * via Requesty.
7
+ * @module backend/agentos/core/llm/providers/implementations/RequestyProvider
8
+ * @implements {IProvider}
9
+ */
10
+ import axios from 'axios';
11
+ import { RequestyProviderError } from '../errors/RequestyProviderError.js';
12
+ import { ApiKeyPool } from '../../../providers/ApiKeyPool.js';
13
+ import { createGMIErrorFromError, GMIErrorCode } from '../../../utils/errors.js'; // Corrected import path
14
+ import { clampMaxOutputTokens } from '../model-output-limits.js';
15
+ export class RequestyProvider {
16
+ constructor() {
17
+ this.providerId = 'requesty';
18
+ this.isInitialized = false;
19
+ this.keyPool = null;
20
+ this.availableModelsCache = new Map();
21
+ }
22
+ async initialize(config) {
23
+ if (!config.apiKey) {
24
+ throw new RequestyProviderError('Requesty API key (apiKey) is required for initialization.', 'INIT_FAILED_MISSING_API_KEY');
25
+ }
26
+ // Corrected: Ensure all properties of Required<RequestyProviderConfig> are present
27
+ // by providing defaults for optional fields before freezing.
28
+ this.config = Object.freeze({
29
+ apiKey: config.apiKey,
30
+ baseURL: config.baseURL || 'https://router.requesty.ai/v1',
31
+ defaultModelId: config.defaultModelId, // Can be undefined, but this.defaultModelId will store it
32
+ siteUrl: config.siteUrl, // Can be undefined
33
+ appName: config.appName, // Can be undefined
34
+ requestTimeout: config.requestTimeout || 60000,
35
+ streamRequestTimeout: config.streamRequestTimeout || 180000,
36
+ });
37
+ this.keyPool = new ApiKeyPool(config.apiKey);
38
+ this.defaultModelId = this.config.defaultModelId; // Store the potentially undefined value
39
+ // No Authorization header here: the key is resolved per request inside
40
+ // makeApiRequest (apiKeyOverride first, then the pool), so key rotation
41
+ // and per call overrides are honored instead of a key baked at init.
42
+ const headers = {
43
+ 'Content-Type': 'application/json',
44
+ 'User-Agent': `AgentOS/1.0 (RequestyProvider; ${this.config.appName || 'UnknownApp'})`,
45
+ };
46
+ if (this.config.siteUrl) {
47
+ headers['HTTP-Referer'] = this.config.siteUrl;
48
+ }
49
+ if (this.config.appName) {
50
+ headers['X-Title'] = this.config.appName;
51
+ }
52
+ this.client = axios.create({
53
+ baseURL: this.config.baseURL,
54
+ headers,
55
+ });
56
+ try {
57
+ await this.refreshAvailableModels();
58
+ this.isInitialized = true;
59
+ console.log(`RequestyProvider initialized. Default Model: ${this.defaultModelId || 'Not set'}. Found ${this.availableModelsCache.size} models via Requesty.`);
60
+ }
61
+ catch (error) {
62
+ this.isInitialized = false;
63
+ const initError = error instanceof RequestyProviderError ? error :
64
+ createGMIErrorFromError(// Corrected: use imported createGMIErrorFromError
65
+ error instanceof Error ? error : new Error(String(error)), GMIErrorCode.LLM_PROVIDER_ERROR, // Corrected: use imported GMIErrorCode
66
+ { providerId: this.providerId }, `RequestyProvider failed to initialize: ${error instanceof Error ? error.message : String(error)}`);
67
+ console.error(initError.message, initError.details || initError);
68
+ throw initError;
69
+ }
70
+ }
71
+ async refreshAvailableModels() {
72
+ const responseData = await this.makeApiRequest('/models', 'GET', this.config.requestTimeout);
73
+ this.availableModelsCache.clear();
74
+ if (responseData && Array.isArray(responseData.data)) {
75
+ responseData.data.forEach((apiModel) => {
76
+ const modelInfo = this.mapApiToModelInfo(apiModel);
77
+ this.availableModelsCache.set(modelInfo.modelId, modelInfo);
78
+ });
79
+ }
80
+ else {
81
+ console.warn("RequestyProvider: Received no model data or malformed response from /models endpoint.");
82
+ }
83
+ }
84
+ mapApiToModelInfo(apiModel) {
85
+ const capabilities = ['chat', 'completion'];
86
+ if (apiModel.architecture?.modality === 'multimodal') {
87
+ capabilities.push('vision_input');
88
+ }
89
+ const knownAdvancedModelPatterns = ['gpt-3.5', 'gpt-4', 'claude-2', 'claude-3', 'gemini', 'mistral', 'llama'];
90
+ if (knownAdvancedModelPatterns.some(pattern => apiModel.id.toLowerCase().includes(pattern))) {
91
+ capabilities.push('tool_use', 'json_mode');
92
+ }
93
+ if (apiModel.id.includes('embedding') || apiModel.id.includes('embed')) {
94
+ capabilities.push('embeddings');
95
+ }
96
+ const parsePrice = (priceStr, tokensFactor = 1000000) => {
97
+ if (typeof priceStr !== 'string')
98
+ return undefined;
99
+ const price = parseFloat(priceStr);
100
+ return isNaN(price) ? undefined : price * tokensFactor;
101
+ };
102
+ return {
103
+ modelId: apiModel.id,
104
+ providerId: this.providerId,
105
+ displayName: apiModel.name,
106
+ description: apiModel.description,
107
+ capabilities: Array.from(new Set(capabilities)),
108
+ contextWindowSize: apiModel.context_length || undefined,
109
+ pricePer1MTokensInput: parsePrice(apiModel.pricing?.prompt),
110
+ pricePer1MTokensOutput: parsePrice(apiModel.pricing?.completion),
111
+ supportsStreaming: true,
112
+ status: 'active',
113
+ };
114
+ }
115
+ ensureInitialized() {
116
+ if (!this.isInitialized) {
117
+ throw new RequestyProviderError('RequestyProvider is not initialized. Please call the initialize() method first.', 'PROVIDER_NOT_INITIALIZED');
118
+ }
119
+ }
120
+ mapToRequestyMessages(messages) {
121
+ return messages.map(msg => {
122
+ const mappedMsg = { role: msg.role, content: msg.content };
123
+ if (msg.name)
124
+ mappedMsg.name = msg.name;
125
+ if (msg.tool_calls)
126
+ mappedMsg.tool_calls = msg.tool_calls;
127
+ if (msg.tool_call_id)
128
+ mappedMsg.tool_call_id = msg.tool_call_id;
129
+ return mappedMsg;
130
+ });
131
+ }
132
+ async generateCompletion(modelId, messages, options) {
133
+ this.ensureInitialized();
134
+ const requestyMessages = this.mapToRequestyMessages(messages);
135
+ const payload = {
136
+ model: modelId,
137
+ messages: requestyMessages,
138
+ stream: false,
139
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
140
+ ...(options.topP !== undefined && { top_p: options.topP }),
141
+ // Requesty reserves credits up to max_tokens at request time. When the
142
+ // caller doesn't specify a limit, it falls back to the model's full output
143
+ // capacity (e.g. 64000 for claude-haiku-4-5), which causes 402 credit-required
144
+ // errors on accounts without enough buffer. Default to 4096, the same value
145
+ // AnthropicProvider uses, so short prompts succeed without explicit tuning.
146
+ // Then clamp to the model's output ceiling so a request sized for a
147
+ // flagship model is not rejected when routed to a lower-ceiling OpenAI
148
+ // model (e.g. openai/gpt-4o caps at 16384, not 32000).
149
+ max_tokens: clampMaxOutputTokens(modelId, options.maxTokens) ?? 4096,
150
+ ...(options.presencePenalty !== undefined && { presence_penalty: options.presencePenalty }),
151
+ ...(options.frequencyPenalty !== undefined && { frequency_penalty: options.frequencyPenalty }),
152
+ ...(options.stopSequences !== undefined && { stop: options.stopSequences }),
153
+ ...(options.userId !== undefined && { user: options.userId }),
154
+ ...(options.tools !== undefined && { tools: options.tools }),
155
+ ...(options.toolChoice !== undefined && { tool_choice: options.toolChoice }),
156
+ ...(options.responseFormat?.type === 'json_object' && { response_format: { type: 'json_object' } }),
157
+ ...(options.customModelParams || {}),
158
+ };
159
+ const apiResponseData = await this.makeApiRequest('/chat/completions', 'POST',
160
+ // CR8: honor a per-call requestTimeout override over the provider default.
161
+ options.requestTimeout ?? this.config.requestTimeout, payload, false, { apiKeyOverride: options.apiKeyOverride, signal: options.abortSignal });
162
+ return this.mapApiToCompletionResponse(apiResponseData, modelId);
163
+ }
164
+ async *generateCompletionStream(modelId, messages, options) {
165
+ this.ensureInitialized();
166
+ const requestyMessages = this.mapToRequestyMessages(messages);
167
+ const payload = {
168
+ model: modelId,
169
+ messages: requestyMessages,
170
+ stream: true,
171
+ // Requesty follows the OpenAI streaming convention: usage is omitted
172
+ // unless stream_options.include_usage is set, in which case a trailing
173
+ // usage-only chunk arrives before [DONE]. Without this flag,
174
+ // streamText({...}).usage resolves to all zeros even on success.
175
+ stream_options: { include_usage: true },
176
+ ...(options.temperature !== undefined && { temperature: options.temperature }),
177
+ ...(options.topP !== undefined && { top_p: options.topP }),
178
+ // Requesty reserves credits up to max_tokens at request time. When the
179
+ // caller doesn't specify a limit, it falls back to the model's full output
180
+ // capacity (e.g. 64000 for claude-haiku-4-5), which causes 402 credit-required
181
+ // errors on accounts without enough buffer. Default to 4096, the same value
182
+ // AnthropicProvider uses, so short prompts succeed without explicit tuning.
183
+ // Then clamp to the model's output ceiling so a request sized for a
184
+ // flagship model is not rejected when routed to a lower-ceiling OpenAI
185
+ // model (e.g. openai/gpt-4o caps at 16384, not 32000).
186
+ max_tokens: clampMaxOutputTokens(modelId, options.maxTokens) ?? 4096,
187
+ ...(options.presencePenalty !== undefined && { presence_penalty: options.presencePenalty }),
188
+ ...(options.frequencyPenalty !== undefined && { frequency_penalty: options.frequencyPenalty }),
189
+ ...(options.stopSequences !== undefined && { stop: options.stopSequences }),
190
+ ...(options.userId !== undefined && { user: options.userId }),
191
+ ...(options.tools !== undefined && { tools: options.tools }),
192
+ ...(options.toolChoice !== undefined && { tool_choice: options.toolChoice }),
193
+ ...(options.responseFormat?.type === 'json_object' && { response_format: { type: 'json_object' } }),
194
+ ...(options.customModelParams || {}),
195
+ };
196
+ const abortSignal = options.abortSignal;
197
+ if (abortSignal?.aborted) {
198
+ yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
199
+ return;
200
+ }
201
+ const accumulatedToolCalls = new Map();
202
+ // Stream contract (see StreamingReconstructor.ts): exactly one chunk per
203
+ // stream carries isFinal:true. With stream_options.include_usage the API
204
+ // sends the finish_reason chunk and then a usage-only chunk, so the
205
+ // finish_reason chunk is buffered here and merged with the usage chunk
206
+ // before a single final chunk is emitted.
207
+ let pendingFinal;
208
+ // Some upstreams routed through Requesty end the stream at [DONE] without
209
+ // ever sending finish_reason. Track whether a final chunk went out so one
210
+ // can be synthesized in that case.
211
+ let finalEmitted = false;
212
+ try {
213
+ const stream = await this.makeApiRequest('/chat/completions', 'POST',
214
+ // CR8: honor a per-call requestTimeout override over the stream default.
215
+ options.requestTimeout ?? this.config.streamRequestTimeout, payload, true, { apiKeyOverride: options.apiKeyOverride, signal: abortSignal });
216
+ for await (const rawChunk of this.parseSseStream(stream)) {
217
+ if (abortSignal?.aborted) {
218
+ pendingFinal = undefined;
219
+ finalEmitted = true;
220
+ yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), modelId, choices: [], error: { message: 'Stream aborted by caller', type: 'abort' }, isFinal: true };
221
+ break;
222
+ }
223
+ if (rawChunk.startsWith('data: ') && rawChunk.includes('[DONE]')) {
224
+ const doneData = rawChunk.substring('data: '.length).trim();
225
+ if (doneData === '[DONE]')
226
+ break;
227
+ }
228
+ if (rawChunk === 'data: [DONE]') {
229
+ break;
230
+ }
231
+ if (rawChunk.startsWith('data: ')) {
232
+ const jsonData = rawChunk.substring('data: '.length);
233
+ let apiChunk;
234
+ try {
235
+ apiChunk = JSON.parse(jsonData);
236
+ }
237
+ catch (error) {
238
+ // Do not log or attach the raw payload: it can hold completion
239
+ // text or tool arguments. Only the length is safe metadata.
240
+ throw new RequestyProviderError(`Malformed SSE data chunk from Requesty (${jsonData.length} bytes could not be parsed as JSON).`, 'STREAM_CHUNK_MALFORMED', undefined, undefined, { payloadLength: jsonData.length, parseError: error instanceof Error ? error.message : String(error) });
241
+ }
242
+ const mapped = this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
243
+ // Don't break on finish_reason: with stream_options.include_usage,
244
+ // Requesty (like OpenAI) emits a trailing usage-only chunk AFTER
245
+ // the finish_reason chunk and BEFORE [DONE]. Breaking here would
246
+ // skip the usage chunk and zero out the caller's token totals. The
247
+ // [DONE] marker check above is the right termination signal.
248
+ if (!mapped.isFinal) {
249
+ yield mapped;
250
+ }
251
+ else if (mapped.choices.length > 0) {
252
+ pendingFinal = mapped;
253
+ }
254
+ else if (pendingFinal) {
255
+ finalEmitted = true;
256
+ yield { ...pendingFinal, usage: mapped.usage ?? pendingFinal.usage };
257
+ pendingFinal = undefined;
258
+ }
259
+ else {
260
+ finalEmitted = true;
261
+ yield mapped;
262
+ }
263
+ }
264
+ }
265
+ if (pendingFinal) {
266
+ // [DONE] (or end of stream) arrived without a usage-only chunk.
267
+ finalEmitted = true;
268
+ yield pendingFinal;
269
+ }
270
+ else if (!finalEmitted) {
271
+ // Stream ended without any finish_reason or usage chunk. Emit the
272
+ // terminal chunk the provider contract requires so consumers see
273
+ // completion.
274
+ yield {
275
+ id: `requesty-final-${Date.now()}`,
276
+ object: 'chat.completion.chunk',
277
+ created: Math.floor(Date.now() / 1000),
278
+ modelId,
279
+ choices: [{ index: 0, message: { role: 'assistant', content: null }, finishReason: 'stop' }],
280
+ isFinal: true,
281
+ };
282
+ }
283
+ }
284
+ catch (error) {
285
+ // Provider contract (IProvider) requires a terminal isFinal:true chunk
286
+ // even on error. If request setup or SSE iteration throws, surface it as
287
+ // a final error chunk instead of letting the generator throw.
288
+ if (abortSignal?.aborted) {
289
+ yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), modelId, choices: [], error: { message: 'Stream aborted by caller', type: 'abort' }, isFinal: true };
290
+ return;
291
+ }
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ yield { id: `requesty-error-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), modelId, choices: [], error: { message, type: 'api_error' }, isFinal: true };
294
+ return;
295
+ }
296
+ }
297
+ async generateEmbeddings(modelId, texts, options) {
298
+ this.ensureInitialized();
299
+ if (!texts || texts.length === 0) {
300
+ throw new RequestyProviderError('Input texts array cannot be empty for generating embeddings.', 'EMBEDDING_NO_INPUT');
301
+ }
302
+ const modelInfo = await this.getModelInfo(modelId);
303
+ if (modelInfo && !modelInfo.capabilities.includes('embeddings')) {
304
+ console.warn(`RequestyProvider: Model '${modelId}' is not explicitly listed with embedding capabilities. Attempting anyway.`);
305
+ }
306
+ const payload = {
307
+ model: modelId,
308
+ input: texts,
309
+ ...(options?.encodingFormat && { encoding_format: options.encodingFormat }),
310
+ ...(options?.dimensions && { dimensions: options.dimensions }),
311
+ ...(options?.inputType && { input_type: options.inputType }),
312
+ ...(options?.customModelParams || {}),
313
+ };
314
+ const apiResponseData = await this.makeApiRequest('/embeddings', 'POST', this.config.requestTimeout, payload, false, { apiKeyOverride: options?.apiKeyOverride });
315
+ if (!Array.isArray(apiResponseData?.data) || !apiResponseData.usage) {
316
+ throw new RequestyProviderError('Malformed embeddings response from Requesty: missing data array or usage.', 'API_RESPONSE_MALFORMED', undefined, undefined, { hasData: Array.isArray(apiResponseData?.data), hasUsage: !!apiResponseData?.usage });
317
+ }
318
+ return {
319
+ object: 'list',
320
+ data: apiResponseData.data.map(d => ({
321
+ object: 'embedding',
322
+ embedding: d.embedding,
323
+ index: d.index,
324
+ })),
325
+ model: apiResponseData.model,
326
+ usage: {
327
+ prompt_tokens: apiResponseData.usage.prompt_tokens,
328
+ total_tokens: apiResponseData.usage.total_tokens,
329
+ },
330
+ };
331
+ }
332
+ async listAvailableModels(filter) {
333
+ this.ensureInitialized();
334
+ if (this.availableModelsCache.size === 0) {
335
+ try {
336
+ await this.refreshAvailableModels();
337
+ }
338
+ catch (refreshError) {
339
+ console.warn("RequestyProvider: Failed to refresh models during listAvailableModels call after finding empty cache:", refreshError);
340
+ }
341
+ }
342
+ const models = Array.from(this.availableModelsCache.values());
343
+ if (filter?.capability) {
344
+ return models.filter(m => m.capabilities.includes(filter.capability));
345
+ }
346
+ return models;
347
+ }
348
+ async getModelInfo(modelId) {
349
+ this.ensureInitialized();
350
+ if (!this.availableModelsCache.has(modelId)) {
351
+ try {
352
+ console.log(`RequestyProvider: Model ${modelId} not in cache. Refreshing model list.`);
353
+ await this.refreshAvailableModels();
354
+ }
355
+ catch (error) {
356
+ console.warn(`RequestyProvider: Failed to refresh models list while trying to get info for ${modelId}:`, error);
357
+ }
358
+ }
359
+ return this.availableModelsCache.get(modelId);
360
+ }
361
+ async checkHealth() {
362
+ if (!this.client) {
363
+ return { isHealthy: false, details: { message: "RequestyProvider not initialized (HTTP client missing)." } };
364
+ }
365
+ try {
366
+ // Auth rides per request since the key is no longer on the axios
367
+ // instance; keep the health probe representative of real traffic.
368
+ await this.client.get('/models', {
369
+ timeout: Math.min(this.config.requestTimeout || 10000, 10000),
370
+ headers: { Authorization: `Bearer ${this.resolveApiKey()}` },
371
+ });
372
+ return { isHealthy: true, details: { message: "Successfully connected to Requesty /models endpoint." } };
373
+ }
374
+ catch (error) {
375
+ const err = error;
376
+ return {
377
+ isHealthy: false,
378
+ details: {
379
+ message: `Requesty health check failed: ${err.message}`,
380
+ status: err.response?.status,
381
+ responseData: err.response?.data,
382
+ },
383
+ };
384
+ }
385
+ }
386
+ async shutdown() {
387
+ this.isInitialized = false;
388
+ this.availableModelsCache.clear();
389
+ console.log('RequestyProvider shutdown: Instance marked as uninitialized and cache cleared.');
390
+ }
391
+ mapApiToCompletionResponse(apiResponse, requestedModelId) {
392
+ const choice = apiResponse.choices[0];
393
+ if (!choice) {
394
+ throw new RequestyProviderError("Received empty choices array from Requesty.", "API_RESPONSE_MALFORMED", undefined, undefined, { responseId: apiResponse.id });
395
+ }
396
+ const choiceWithoutMessage = apiResponse.choices.find(c => !c.message || typeof c.message !== 'object');
397
+ if (choiceWithoutMessage) {
398
+ throw new RequestyProviderError(`Choice ${choiceWithoutMessage.index} in Requesty response has no message object.`, "API_RESPONSE_MALFORMED", undefined, undefined, { responseId: apiResponse.id, choiceIndex: choiceWithoutMessage.index });
399
+ }
400
+ const usage = apiResponse.usage ? {
401
+ promptTokens: apiResponse.usage.prompt_tokens,
402
+ completionTokens: apiResponse.usage.completion_tokens,
403
+ totalTokens: apiResponse.usage.total_tokens,
404
+ costUSD: apiResponse.usage.cost,
405
+ } : undefined;
406
+ return {
407
+ id: apiResponse.id,
408
+ object: apiResponse.object,
409
+ created: apiResponse.created,
410
+ modelId: apiResponse.model || requestedModelId,
411
+ choices: apiResponse.choices.map(c => ({
412
+ index: c.index,
413
+ message: {
414
+ role: c.message.role,
415
+ content: c.message.content,
416
+ tool_calls: c.message.tool_calls,
417
+ },
418
+ finishReason: c.finish_reason,
419
+ logprobs: c.logprobs,
420
+ })),
421
+ usage,
422
+ };
423
+ }
424
+ mapApiToStreamChunkResponse(apiChunk, requestedModelId, accumulatedToolCalls) {
425
+ const choice = apiChunk.choices[0];
426
+ // With stream_options.include_usage, Requesty (like OpenAI) emits a
427
+ // trailing chunk with an empty choices array and a populated usage
428
+ // object. Recognize it as a final usage-only chunk so callers see real
429
+ // token totals after the stream resolves. Without this, the empty-choices
430
+ // path below would mark it as a malformed-response error.
431
+ if ((!apiChunk.choices || apiChunk.choices.length === 0) && apiChunk.usage) {
432
+ return {
433
+ id: apiChunk.id,
434
+ object: apiChunk.object,
435
+ created: apiChunk.created,
436
+ modelId: apiChunk.model || requestedModelId,
437
+ choices: [],
438
+ isFinal: true,
439
+ usage: {
440
+ promptTokens: apiChunk.usage.prompt_tokens,
441
+ completionTokens: apiChunk.usage.completion_tokens,
442
+ totalTokens: apiChunk.usage.total_tokens,
443
+ costUSD: apiChunk.usage.cost,
444
+ },
445
+ };
446
+ }
447
+ if (!choice) {
448
+ return {
449
+ id: apiChunk.id, object: apiChunk.object, created: apiChunk.created,
450
+ modelId: apiChunk.model || requestedModelId, choices: [], isFinal: true,
451
+ error: { message: "Stream chunk contained no choices.", type: "invalid_response" }
452
+ };
453
+ }
454
+ let responseTextDelta;
455
+ let toolCallsDeltas;
456
+ if (choice.delta?.content) {
457
+ responseTextDelta = choice.delta.content;
458
+ }
459
+ if (choice.delta?.tool_calls) {
460
+ toolCallsDeltas = [];
461
+ choice.delta.tool_calls.forEach(tcDelta => {
462
+ let currentToolCallState = accumulatedToolCalls.get(tcDelta.index);
463
+ if (!currentToolCallState) {
464
+ currentToolCallState = { function: { name: '', arguments: '' } };
465
+ }
466
+ if (tcDelta.id)
467
+ currentToolCallState.id = tcDelta.id;
468
+ if (tcDelta.type)
469
+ currentToolCallState.type = tcDelta.type;
470
+ if (tcDelta.function?.name)
471
+ currentToolCallState.function.name = (currentToolCallState.function.name || '') + tcDelta.function.name;
472
+ if (tcDelta.function?.arguments)
473
+ currentToolCallState.function.arguments = (currentToolCallState.function.arguments || '') + tcDelta.function.arguments;
474
+ accumulatedToolCalls.set(tcDelta.index, currentToolCallState);
475
+ toolCallsDeltas.push({
476
+ index: tcDelta.index,
477
+ id: tcDelta.id,
478
+ type: tcDelta.type,
479
+ function: tcDelta.function ? {
480
+ name: tcDelta.function.name,
481
+ arguments_delta: tcDelta.function.arguments
482
+ } : undefined,
483
+ });
484
+ });
485
+ }
486
+ const isFinal = !!choice.finish_reason;
487
+ let finalUsage;
488
+ const finalChoices = [];
489
+ if (isFinal) {
490
+ if (apiChunk.usage) {
491
+ finalUsage = {
492
+ promptTokens: apiChunk.usage.prompt_tokens,
493
+ completionTokens: apiChunk.usage.completion_tokens,
494
+ totalTokens: apiChunk.usage.total_tokens,
495
+ costUSD: apiChunk.usage.cost,
496
+ };
497
+ }
498
+ const finalMessage = {
499
+ role: choice.delta?.role || (accumulatedToolCalls.size > 0 ? 'assistant' : (choice.message?.role || 'assistant')),
500
+ content: responseTextDelta || (choice.message?.content || null),
501
+ tool_calls: Array.from(accumulatedToolCalls.values())
502
+ .filter(tc => tc.id && tc.function?.name)
503
+ .map(accTc => ({
504
+ id: accTc.id,
505
+ type: accTc.type,
506
+ function: { name: accTc.function.name, arguments: accTc.function.arguments }
507
+ })),
508
+ };
509
+ if (!finalMessage.tool_calls || finalMessage.tool_calls.length === 0) {
510
+ delete finalMessage.tool_calls;
511
+ }
512
+ if (responseTextDelta && !choice.message?.content && accumulatedToolCalls.size === 0) {
513
+ finalMessage.content = responseTextDelta;
514
+ }
515
+ else if (accumulatedToolCalls.size > 0 && !responseTextDelta && !choice.message?.content) {
516
+ finalMessage.content = null;
517
+ }
518
+ finalChoices.push({
519
+ index: choice.index,
520
+ message: finalMessage,
521
+ finishReason: choice.finish_reason,
522
+ logprobs: choice.logprobs,
523
+ });
524
+ }
525
+ else {
526
+ finalChoices.push({
527
+ index: choice.index,
528
+ message: {
529
+ role: choice.delta?.role || 'assistant',
530
+ content: responseTextDelta || null,
531
+ },
532
+ finishReason: null,
533
+ });
534
+ }
535
+ return {
536
+ id: apiChunk.id,
537
+ object: apiChunk.object,
538
+ created: apiChunk.created,
539
+ modelId: apiChunk.model || requestedModelId,
540
+ choices: finalChoices,
541
+ responseTextDelta: isFinal ? undefined : responseTextDelta,
542
+ toolCallsDeltas: isFinal ? undefined : toolCallsDeltas,
543
+ isFinal,
544
+ usage: finalUsage,
545
+ };
546
+ }
547
+ /**
548
+ * Resolve the bearer key for one request: a per call override wins,
549
+ * otherwise the next key from the pool (weighted rotation).
550
+ */
551
+ resolveApiKey(apiKeyOverride) {
552
+ if (apiKeyOverride)
553
+ return apiKeyOverride;
554
+ return this.keyPool?.hasKeys ? this.keyPool.next() : this.config.apiKey;
555
+ }
556
+ /**
557
+ * Reduce an unknown thrown value to fields that are safe to attach to
558
+ * error details and to log. The raw Axios error keeps the request config
559
+ * (Authorization header, serialized body), so it is never stored as is.
560
+ */
561
+ sanitizeUnderlyingError(error) {
562
+ if (error instanceof Error) {
563
+ const code = error.code;
564
+ return {
565
+ name: error.name,
566
+ ...(typeof code === 'string' ? { code } : {}),
567
+ message: error.message,
568
+ };
569
+ }
570
+ return { message: String(error) };
571
+ }
572
+ async makeApiRequest(endpoint, method, timeout, body, expectStream = false, perRequest) {
573
+ try {
574
+ const response = await this.client.request({
575
+ url: endpoint,
576
+ method,
577
+ data: body,
578
+ timeout: timeout,
579
+ headers: { Authorization: `Bearer ${this.resolveApiKey(perRequest?.apiKeyOverride)}` },
580
+ // Forward the caller's AbortSignal so an abort settles the request
581
+ // while Axios is still waiting for headers or the next body chunk.
582
+ ...(perRequest?.signal ? { signal: perRequest.signal } : {}),
583
+ responseType: expectStream ? 'stream' : 'json',
584
+ });
585
+ return response.data;
586
+ }
587
+ catch (error) {
588
+ let statusCode;
589
+ let errorData;
590
+ let errorMessage = 'Unknown Requesty API error';
591
+ let errorType = 'UNKNOWN_API_ERROR';
592
+ if (axios.isAxiosError(error)) {
593
+ statusCode = error.response?.status;
594
+ errorData = error.response?.data;
595
+ if (errorData?.error && typeof errorData.error === 'object') {
596
+ errorMessage = errorData.error.message || errorMessage;
597
+ errorType = errorData.error.type || errorType;
598
+ }
599
+ else if (typeof errorData === 'string') {
600
+ errorMessage = errorData;
601
+ }
602
+ else if (error.message) {
603
+ errorMessage = error.message;
604
+ }
605
+ }
606
+ else if (error instanceof Error) {
607
+ errorMessage = error.message;
608
+ }
609
+ // Prefix the status code into the message so downstream retry/fallback
610
+ // logic (e.g. isRetryableError, which greps for \b402\b) can route on it
611
+ // even when the Requesty API body provides a friendlier description.
612
+ const decoratedMessage = statusCode ? `[${statusCode}] ${errorMessage}` : errorMessage;
613
+ throw new RequestyProviderError(decoratedMessage, 'API_REQUEST_FAILED', statusCode, errorType, { requestEndpoint: endpoint, requestBodyKeys: body ? Object.keys(body) : undefined, responseData: errorData, underlyingError: this.sanitizeUnderlyingError(error) });
614
+ }
615
+ }
616
+ async *parseSseStream(stream) {
617
+ let buffer = '';
618
+ const readableStream = stream;
619
+ try {
620
+ for await (const chunk of readableStream) {
621
+ buffer += chunk.toString();
622
+ let eolIndex;
623
+ while ((eolIndex = buffer.indexOf('\n')) >= 0) {
624
+ const line = buffer.substring(0, eolIndex).trim();
625
+ buffer = buffer.substring(eolIndex + 1);
626
+ if (line) {
627
+ yield line;
628
+ }
629
+ }
630
+ }
631
+ if (buffer.trim()) {
632
+ yield buffer.trim();
633
+ }
634
+ }
635
+ catch (error) {
636
+ const message = error instanceof Error ? error.message : "Requesty stream parsing/reading error";
637
+ console.error("RequestyProvider: Error reading or parsing SSE stream:", message);
638
+ if (error instanceof RequestyProviderError)
639
+ throw error;
640
+ throw new RequestyProviderError(message, 'STREAM_PARSING_ERROR', undefined, undefined, this.sanitizeUnderlyingError(error));
641
+ }
642
+ finally {
643
+ if (typeof readableStream.destroy === 'function') {
644
+ readableStream.destroy();
645
+ }
646
+ else if (typeof readableStream.close === 'function') {
647
+ readableStream.close();
648
+ }
649
+ }
650
+ }
651
+ }
652
+ //# sourceMappingURL=RequestyProvider.js.map