@callmedayz/ai-prompt-toolkit
Version:
Professional AI prompt engineering toolkit with advanced template features, real-time dashboards, conditional logic, template inheritance, live monitoring, OpenRouter integration, and 310+ model support
341 lines • 12.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenRouterClient = void 0;
const error_handling_1 = require("./error-handling");
const rate_limiting_1 = require("./rate-limiting");
/**
* OpenRouter API client for making authenticated requests
*/
class OpenRouterClient {
constructor(config, retryConfig, rateLimitConfig, quotaConfig) {
this.config = {
apiKey: config.apiKey,
baseUrl: config.baseUrl || 'https://openrouter.ai/api/v1',
timeout: config.timeout || 30000,
retries: config.retries || 3
};
this.retryConfig = { ...error_handling_1.DEFAULT_RETRY_CONFIG, ...retryConfig };
this.circuitBreaker = new error_handling_1.CircuitBreaker();
// Initialize rate limiter if config provided
if (rateLimitConfig) {
this.rateLimiter = new rate_limiting_1.RateLimiter(rateLimitConfig);
}
// Initialize quota manager if config provided
if (quotaConfig) {
this.quotaManager = new rate_limiting_1.QuotaManager(quotaConfig);
}
if (!this.config.apiKey) {
throw new error_handling_1.OpenRouterError('OpenRouter API key is required', 'authentication', undefined, false);
}
}
/**
* Make a completion request to OpenRouter
*/
async completion(request) {
if (request.stream) {
throw new Error('Use completionStream() for streaming requests');
}
// Estimate tokens and cost for rate limiting
const estimatedTokens = this.estimateTokens(request);
const estimatedCost = this.estimateCost(request);
const response = await this.makeRequest('/chat/completions', {
method: 'POST',
body: JSON.stringify(request)
}, estimatedTokens, estimatedCost);
// Record actual usage
this.recordUsage(response.usage.total_tokens, this.calculateActualCost(response));
return response;
}
/**
* Make a streaming completion request to OpenRouter
*/
async completionStream(request) {
const streamRequest = { ...request, stream: true };
const url = `${this.config.baseUrl}/chat/completions`;
const headers = {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/callmedayz/ai-prompt-toolkit',
'X-Title': 'AI Prompt Toolkit'
};
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(streamRequest)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OpenRouter streaming error (${response.status}): ${errorText}`);
}
if (!response.body) {
throw new Error('No response body for streaming request');
}
return this.createStreamParser(response.body);
}
/**
* Get token count for text using OpenRouter's completion API
* This makes a minimal completion request to get accurate token counts
*/
async tokenize(request) {
// Make a minimal completion request to get token count
const completionRequest = {
model: request.model,
messages: [{ role: 'user', content: request.text }],
max_tokens: 1, // Minimal completion to just get token count
temperature: 0
};
try {
const response = await this.completion(completionRequest);
return {
tokens: response.usage.prompt_tokens,
token_ids: undefined // Not available through this method
};
}
catch (error) {
throw new Error(`Tokenization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get available models from OpenRouter
*/
async getModels() {
return this.makeRequest('/models', {
method: 'GET'
});
}
/**
* Get generation details including native token counts
*/
async getGeneration(id) {
return this.makeRequest(`/generation?id=${id}`, {
method: 'GET'
});
}
/**
* Make an authenticated request to OpenRouter API with enhanced error handling
*/
async makeRequest(endpoint, options, estimatedTokens = 0, estimatedCost = 0) {
// Check rate limits
if (this.rateLimiter) {
const rateLimitStatus = this.rateLimiter.checkRequest(estimatedTokens, estimatedCost);
if (!rateLimitStatus.allowed) {
throw new error_handling_1.OpenRouterError(rateLimitStatus.reason || 'Rate limit exceeded', 'rate_limit', 429, true, rateLimitStatus.retryAfter);
}
}
// Check quota
if (this.quotaManager) {
const quotaStatus = this.quotaManager.checkQuota(estimatedCost);
if (!quotaStatus.allowed) {
throw new error_handling_1.OpenRouterError(quotaStatus.alert?.message || 'Quota exceeded', 'quota_exceeded', 402, false);
}
}
return this.circuitBreaker.execute(async () => {
return (0, error_handling_1.retryWithBackoff)(async () => {
const url = `${this.config.baseUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/callmedayz/ai-prompt-toolkit',
'X-Title': 'AI Prompt Toolkit'
};
const requestOptions = {
...options,
headers: {
...headers,
...options.headers
}
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
try {
const response = await fetch(url, {
...requestOptions,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw (0, error_handling_1.parseError)(response, errorText);
}
return await response.json();
}
catch (error) {
clearTimeout(timeoutId);
throw error;
}
}, this.retryConfig);
});
}
/**
* Parse Server-Sent Events stream from OpenRouter
*/
createStreamParser(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
return new ReadableStream({
async start(controller) {
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith(':')) {
continue;
}
// Parse SSE format: "data: {...}"
if (trimmed.startsWith('data: ')) {
const data = trimmed.slice(6);
// Check for end of stream
if (data === '[DONE]') {
controller.close();
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
controller.enqueue(content);
}
}
catch (parseError) {
console.warn('Failed to parse streaming data:', data);
}
}
}
}
}
catch (error) {
controller.error(error);
}
finally {
reader.releaseLock();
}
controller.close();
}
});
}
/**
* Test the API connection and authentication
*/
async testConnection() {
try {
await this.getModels();
return true;
}
catch (error) {
return false;
}
}
/**
* Get API key from environment or throw error
*/
static getApiKeyFromEnv() {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new Error('OPENROUTER_API_KEY environment variable is required. ' +
'Get your API key from https://openrouter.ai/keys');
}
return apiKey;
}
/**
* Get circuit breaker status
*/
getCircuitBreakerStatus() {
return this.circuitBreaker.getState();
}
/**
* Update retry configuration
*/
updateRetryConfig(config) {
this.retryConfig = { ...this.retryConfig, ...config };
}
/**
* Get current retry configuration
*/
getRetryConfig() {
return { ...this.retryConfig };
}
/**
* Estimate tokens for a request (rough estimation)
*/
estimateTokens(request) {
const text = request.messages.map(m => m.content).join(' ');
return Math.ceil(text.length / 4); // Rough estimation: 1 token ≈ 4 characters
}
/**
* Estimate cost for a request (very rough estimation)
*/
estimateCost(request) {
const tokens = this.estimateTokens(request);
// Very rough cost estimation - would need model-specific pricing
return tokens * 0.00002; // Rough estimate for GPT-3.5-turbo pricing
}
/**
* Calculate actual cost from response
*/
calculateActualCost(response) {
// This would need model-specific pricing data
// For now, use rough estimation
return response.usage.total_tokens * 0.00002;
}
/**
* Record usage in rate limiter and quota manager
*/
recordUsage(tokens, cost) {
if (this.rateLimiter) {
this.rateLimiter.recordUsage(tokens, cost);
}
if (this.quotaManager) {
this.quotaManager.recordSpending(cost);
}
}
/**
* Get rate limiting status
*/
getRateLimitStatus() {
return this.rateLimiter?.getUsage();
}
/**
* Get quota status
*/
getQuotaStatus() {
return this.quotaManager?.getUsage();
}
/**
* Get quota alerts
*/
getQuotaAlerts() {
return this.quotaManager?.getAlerts() || [];
}
/**
* Enable rate limiting
*/
enableRateLimiting(config) {
this.rateLimiter = new rate_limiting_1.RateLimiter(config);
}
/**
* Enable quota management
*/
enableQuotaManagement(config) {
this.quotaManager = new rate_limiting_1.QuotaManager(config);
}
/**
* Create a client instance using environment variables
*/
static fromEnv(config, retryConfig, rateLimitConfig, quotaConfig) {
return new OpenRouterClient({
apiKey: this.getApiKeyFromEnv(),
...config
}, retryConfig, rateLimitConfig, quotaConfig);
}
}
exports.OpenRouterClient = OpenRouterClient;
//# sourceMappingURL=openrouter-client.js.map