/**
* Requesty AI Adapter with true streaming support
* OpenAI-compatible streaming interface for 150+ models via router
* Based on Requesty streaming documentation
*/
import { BaseAdapter } from '../BaseAdapter';
import {
GenerateOptions,
StreamChunk,
LLMResponse,
ModelInfo,
ProviderCapabilities,
ModelPricing
} from '../types';
import { REQUESTY_MODELS, REQUESTY_DEFAULT_MODEL } from './RequestyModels';
import { MCPToolExecution } from '../shared/ToolExecutionUtils';
/**
* Requesty API response structure (OpenAI-compatible)
*/
interface RequestyChatCompletionResponse {
choices: Array<{
message?: {
content?: string;
toolCalls?: any[];
};
finish_reason?: string;
}>;
usage?: {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
};
}
export class RequestyAdapter extends BaseAdapter {
readonly name = 'requesty';
readonly baseUrl = 'https://router.requesty.ai/v1';
constructor(apiKey: string, model?: string) {
super(apiKey, model || REQUESTY_DEFAULT_MODEL);
this.initializeCache();
}
async generateUncached(prompt: string, options?: GenerateOptions): Promise<LLMResponse> {
try {
const model = options?.model || this.currentModel;
// Tool execution requires streaming - use generateStreamAsync instead
if (options?.tools && options.tools.length > 0) {
throw new Error('Tool execution requires streaming. Use generateStreamAsync() instead.');
}
// Use basic chat completions
return await this.generateWithChatCompletions(prompt, options);
} catch (error) {
throw this.handleError(error, 'generation');
}
}
/**
* Generate streaming response using async generator
* Uses unified stream processing with automatic tool call accumulation
*/
async* generateStreamAsync(prompt: string, options?: GenerateOptions): AsyncGenerator<StreamChunk, void, unknown> {
try {
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
...this.buildHeaders(),
'Authorization': `Bearer ${this.apiKey}`,
'HTTP-Referer': 'https://synaptic-lab-kit.com',
'X-Title': 'Synaptic Lab Kit',
'User-Agent': 'Synaptic-Lab-Kit/1.0.0'
},
body: JSON.stringify({
model: options?.model || this.currentModel,
messages: this.buildMessages(prompt, options?.systemPrompt),
temperature: options?.temperature,
max_tokens: options?.maxTokens,
response_format: options?.jsonMode ? { type: 'json_object' } : undefined,
stop: options?.stopSequences,
tools: options?.tools,
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
// Use unified stream processing with automatic SSE parsing and tool call accumulation
yield* this.processStream(response, {
debugLabel: 'Requesty',
extractContent: (parsed) => parsed.choices[0]?.delta?.content || null,
extractToolCalls: (parsed) => parsed.choices[0]?.delta?.tool_calls || null,
extractFinishReason: (parsed) => parsed.choices[0]?.finish_reason || null,
extractUsage: (parsed) => parsed.usage || null
});
} catch (error) {
console.error('[RequestyAdapter] Streaming error:', error);
throw error;
}
}
async listModels(): Promise<ModelInfo[]> {
try {
return REQUESTY_MODELS.map(model => ({
id: model.apiName,
name: model.name,
contextWindow: model.contextWindow,
maxOutputTokens: model.maxTokens,
supportsJSON: model.capabilities.supportsJSON,
supportsImages: model.capabilities.supportsImages,
supportsFunctions: model.capabilities.supportsFunctions,
supportsStreaming: model.capabilities.supportsStreaming,
supportsThinking: false,
costPer1kTokens: {
input: model.inputCostPerMillion / 1000,
output: model.outputCostPerMillion / 1000
},
pricing: {
inputPerMillion: model.inputCostPerMillion,
outputPerMillion: model.outputCostPerMillion,
currency: 'USD',
lastUpdated: new Date().toISOString()
}
}));
} catch (error) {
this.handleError(error, 'listing models');
return [];
}
}
getCapabilities(): ProviderCapabilities {
const baseCapabilities = {
supportsStreaming: true,
supportsJSON: true,
supportsImages: true,
supportsFunctions: true,
supportsThinking: false,
maxContextWindow: 200000,
supportedFeatures: [
'messages',
'function_calling',
'vision',
'streaming',
'json_mode',
'router_fallback'
]
};
return baseCapabilities;
}
/**
* Generate using standard chat completions
*/
private async generateWithChatCompletions(prompt: string, options?: GenerateOptions): Promise<LLMResponse> {
const model = options?.model || this.currentModel;
const requestBody: any = {
model,
messages: this.buildMessages(prompt, options?.systemPrompt),
temperature: options?.temperature,
max_tokens: options?.maxTokens,
response_format: options?.jsonMode ? { type: 'json_object' } : undefined,
stop: options?.stopSequences
};
// Add tools if provided
if (options?.tools) {
requestBody.tools = options.tools;
}
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
...this.buildHeaders(),
'Authorization': `Bearer ${this.apiKey}`,
'HTTP-Referer': 'https://synaptic-lab-kit.com',
'X-Title': 'Synaptic Lab Kit',
'User-Agent': 'Synaptic-Lab-Kit/1.0.0'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json() as RequestyChatCompletionResponse;
const choice = data.choices[0];
if (!choice) {
throw new Error('No response from Requesty');
}
let text = choice.message?.content || '';
const usage = this.extractUsage(data);
const finishReason = this.mapFinishReason(choice.finish_reason || null);
// If tools were provided and we got tool calls, return placeholder text
if (options?.tools && choice.message?.toolCalls && choice.message.toolCalls.length > 0) {
text = text || '[AI requested tool calls but tool execution not available]';
}
return this.buildLLMResponse(
text,
model,
usage,
{ provider: 'requesty' },
finishReason
);
}
// Private methods
private extractToolCalls(message: any): any[] {
return message?.toolCalls || [];
}
private mapFinishReason(reason: string | null): 'stop' | 'length' | 'tool_calls' | 'content_filter' {
if (!reason) return 'stop';
const reasonMap: Record<string, 'stop' | 'length' | 'tool_calls' | 'content_filter'> = {
'stop': 'stop',
'length': 'length',
'tool_calls': 'tool_calls',
'content_filter': 'content_filter'
};
return reasonMap[reason] || 'stop';
}
protected extractUsage(response: any): any {
const usage = response.usage;
if (usage) {
return {
promptTokens: usage.prompt_tokens || 0,
completionTokens: usage.completion_tokens || 0,
totalTokens: usage.total_tokens || 0
};
}
return undefined;
}
private getCostPer1kTokens(modelId: string): { input: number; output: number } | undefined {
const model = REQUESTY_MODELS.find(m => m.apiName === modelId);
if (!model) return undefined;
return {
input: model.inputCostPerMillion / 1000,
output: model.outputCostPerMillion / 1000
};
}
async getModelPricing(modelId: string): Promise<ModelPricing | null> {
const costs = this.getCostPer1kTokens(modelId);
if (!costs) return null;
return {
rateInputPerMillion: costs.input * 1000,
rateOutputPerMillion: costs.output * 1000,
currency: 'USD'
};
}
}