askexperts
Version:
AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol
177 lines • 7.13 kB
JavaScript
import { encode } from "gpt-tokenizer";
import { debugExpert } from "../common/debug.js";
/**
* Helper function to process an AsyncIterable with a side effect
* while passing through the original items unchanged
*/
async function* tapAsyncIterable(source, inspect) {
for await (const item of source) {
await inspect(item); // run your side-effect or inspection
yield item; // pass the item through unchanged
}
}
/**
* OpenAI interface implementation that uses OpenRouter
* Provides pricing and estimation functions
*/
export class OpenaiOpenRouter {
/**
* Creates a new OpenaiOpenRouter instance
*
* @param openai - The underlying OpenAI client
* @param openRouter - The OpenRouter instance for pricing
* @param margin - Profit margin (default: 0)
*/
constructor(openai, openRouter, margin = 0) {
/**
* Average output token count for pricing estimates
*/
this.avgOutputCount = 300;
/**
* Number of outputs processed (for averaging)
*/
this.outputCount = 1;
/**
* Map of active quotes with their model and content
*/
this.activeQuotes = new Map();
this.openai = openai;
this.openRouter = openRouter;
this.margin = margin;
}
/**
* Gets pricing information for a model in sats per million tokens
* Delegates to the OpenRouter instance
*
* @param model - Model ID
* @returns Promise resolving to pricing information or undefined if not available
*/
async pricing(model) {
try {
return await this.openRouter.pricing(model);
}
catch (error) {
console.error("Error getting pricing:", error);
return undefined;
}
}
/**
* Count tokens using gpt-tokenizer
*
* @param text - Text to count tokens for
* @returns Token count
*/
countTokens(text) {
return encode(text).length;
}
/**
* Estimates the price of processing a prompt
* Implements the logic from OpenaiProxyExpertBase.onPromptPrice
*
* @param model - Model ID
* @param content - The chat completion parameters
* @returns Promise resolving to the estimated price object
*/
async getQuote(model, content) {
try {
// Calculate the number of tokens in the content
let inputTokenCount = 0;
// For OpenAI format, count tokens in each message
inputTokenCount = this.countTokens(JSON.stringify(content.messages));
// Use the average output count for pricing
const outputTokenCount = this.avgOutputCount;
// Get current pricing
const pricing = await this.pricing(model);
if (!pricing) {
// Generate a unique quote ID even for error cases
const quoteId = `openrouter-error-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
return { amountSats: 0, quoteId };
}
// Calculate the price in sats
const inputPrice = (inputTokenCount * pricing.inputPricePPM) / 1000000;
const outputPrice = (outputTokenCount * pricing.outputPricePPM) / 1000000;
const totalPrice = Math.ceil((inputPrice + outputPrice) * (1 + this.margin));
// Generate a unique quote ID
const quoteId = `openrouter-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
// Store the model and content in the activeQuotes map
this.activeQuotes.set(quoteId, {
model,
content
});
return {
amountSats: totalPrice,
quoteId
};
}
catch (error) {
console.error("Error estimating price:", error);
// Generate a unique quote ID for error cases
const quoteId = `openrouter-error-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
return { amountSats: 0, quoteId };
}
}
/**
* Execute a chat completion request
* Implementation of the interface method
*
* @param quoteId - Quote ID for the request
* @param options - Additional options for the request
* @returns Promise resolving to chat completion or chunks
*/
execute(quoteId, options) {
// Get the stored model and content from the activeQuotes map
const quoteData = this.activeQuotes.get(quoteId);
if (!quoteData) {
throw new Error(`No active quote found for ID: ${quoteId}`);
}
// Use the stored content
const body = quoteData.content;
// Handle streaming responses
if (body.stream === true) {
// Call the underlying OpenAI client to get the stream
const resultPromise = this.openai.chat.completions.create(body, options);
// Return a new promise that will resolve to a wrapped AsyncIterable
return resultPromise.then(stream => {
let accumulatedContent = "";
// Use tapAsyncIterable to process each chunk while passing it through
return tapAsyncIterable(stream, async (chunk) => {
// Accumulate the content from each chunk
accumulatedContent += chunk.choices[0]?.delta?.content || "";
// When we receive the last chunk, update the average output count
if (chunk.choices[0]?.finish_reason !== null) {
this.updateAverageOutputCount(accumulatedContent);
}
});
});
}
else {
// Handle non-streaming responses
const result = this.openai.chat.completions.create(body, options);
// Update the average output token count when the result is available
result.then(response => {
if ('choices' in response) {
const output = response.choices[0]?.message?.content || "";
this.updateAverageOutputCount(output);
}
}).catch(error => {
console.error("Error processing completion result:", error);
});
return result;
}
}
/**
* Updates the average output token count based on new output
*
* @param outputContent - The content to count tokens for
*/
updateAverageOutputCount(outputContent) {
const outputTokenCount = this.countTokens(outputContent);
// Update the average using the formula: avgOutputTokens = (avgOutputTokens * outputCount + newOutputCount) / (outputCount + 1)
this.avgOutputCount =
(this.avgOutputCount * this.outputCount + outputTokenCount) /
(this.outputCount + 1);
this.outputCount++;
debugExpert(`Updated average output token count: ${this.avgOutputCount} (based on ${this.outputCount} outputs)`);
}
}
//# sourceMappingURL=OpenaiOpenRouter.js.map