aiwrapper
Version:
A Universal AI Wrapper for JavaScript & TypeScript
185 lines • 8.75 kB
JavaScript
import { httpRequestWithRetry as fetch, } from "../../http-request.js";
import { processResponseStream } from "../../process-response-stream.js";
import { LangResultWithMessages, LanguageProvider, } from "../language-provider.js";
import { models } from 'aimodels';
import { calculateModelResponseTokens } from "../utils/token-calculator.js";
export class AnthropicLang extends LanguageProvider {
constructor(options) {
const modelName = options.model || "claude-3-sonnet-20240229";
super(modelName);
// Get model info from aimodels
const modelInfo = models.id(modelName);
if (!modelInfo) {
console.error(`Invalid Anthropic model: ${modelName}. Model not found in aimodels database.`);
}
this._config = {
apiKey: options.apiKey,
model: modelName,
systemPrompt: options.systemPrompt,
maxTokens: options.maxTokens,
extendedThinking: options.extendedThinking,
};
}
async ask(prompt, onResult) {
const messages = [];
if (this._config.systemPrompt) {
messages.push({
role: "system",
content: this._config.systemPrompt,
});
}
messages.push({
role: "user",
content: prompt,
});
return await this.chat(messages, onResult);
}
async chat(messages, onResult) {
// Remove all system messages, save the first one if it exists.
let detectedSystemMessage = "";
messages = messages.filter((message) => {
if (message.role === "system") {
if (!detectedSystemMessage) {
// Saving the first system message.
detectedSystemMessage = message.content;
}
return false;
}
return true;
});
const result = new LangResultWithMessages(messages);
// Get model info and calculate max tokens
const modelInfo = models.id(this._config.model);
if (!modelInfo) {
throw new Error(`Model info not found for ${this._config.model}`);
}
const requestMaxTokens = calculateModelResponseTokens(modelInfo, messages, this._config.maxTokens);
// Track if we're receiving thinking content
let isReceivingThinking = false;
let thinkingContent = "";
const onData = (data) => {
var _a;
if (data.type === "message_stop") {
// Store the thinking content in the result object before finishing
result.thinking = thinkingContent;
result.finished = true;
onResult === null || onResult === void 0 ? void 0 : onResult(result);
return;
}
// Handle thinking content
if (data.type === "content_block_start" && ((_a = data.content_block) === null || _a === void 0 ? void 0 : _a.type) === "thinking") {
isReceivingThinking = true;
return;
}
if (data.type === "content_block_stop" && isReceivingThinking) {
isReceivingThinking = false;
// Update the thinking content in the result object
result.thinking = thinkingContent;
onResult === null || onResult === void 0 ? void 0 : onResult(result);
return;
}
if (data.type === "message_delta" && data.delta.stop_reason === "end_turn") {
const choices = data.delta.choices;
if (choices && choices.length > 0) {
const deltaContent = choices[0].delta.content
? choices[0].delta.content
: "";
result.answer += deltaContent;
result.messages = [
...messages,
{
role: "assistant",
content: result.answer,
},
];
onResult === null || onResult === void 0 ? void 0 : onResult(result);
}
}
if (data.type === "content_block_delta") {
// Handle thinking content delta
if (data.delta.type === "thinking_delta" && data.delta.thinking) {
thinkingContent += data.delta.thinking;
// Update the thinking content in the result object
result.thinking = thinkingContent;
onResult === null || onResult === void 0 ? void 0 : onResult(result);
return;
}
// Handle regular text delta
const deltaContent = data.delta.text ? data.delta.text : "";
// If we're receiving thinking content, store it separately
if (isReceivingThinking) {
thinkingContent += deltaContent;
// Update the thinking content in the result object
result.thinking = thinkingContent;
onResult === null || onResult === void 0 ? void 0 : onResult(result);
return;
}
result.answer += deltaContent;
onResult === null || onResult === void 0 ? void 0 : onResult(result);
}
};
// Check if the model supports extended thinking by looking for the "reason" capability
const supportsExtendedThinking = modelInfo.can && modelInfo.can("reason");
// Prepare request body
const requestBody = {
model: this._config.model,
messages: messages,
max_tokens: requestMaxTokens,
system: this._config.systemPrompt ? this._config.systemPrompt : detectedSystemMessage,
stream: true,
};
// Add extended thinking if enabled and supported
if (this._config.extendedThinking && supportsExtendedThinking) {
// Calculate a reasonable thinking budget
// According to Anthropic's docs, max_tokens must be greater than thinking.budget_tokens
// So we'll set thinking budget to be 75% of max_tokens
let thinkingBudget = Math.floor(requestMaxTokens * 0.75);
// Check if the model has extended reasoning info
// Using type assertion to avoid TypeScript errors since the aimodels types might not include the extended property
const contextObj = modelInfo.context;
if (contextObj && contextObj.extended && contextObj.extended.reasoning && contextObj.extended.reasoning.maxOutput) {
// Make sure thinking budget doesn't exceed the model's capabilities
thinkingBudget = Math.min(thinkingBudget, Math.floor(contextObj.extended.reasoning.maxOutput * 0.75));
}
// Make sure thinking budget is at least 1000 tokens for meaningful reasoning
thinkingBudget = Math.max(thinkingBudget, 1000);
// Ensure max_tokens is greater than thinking.budget_tokens
requestBody.max_tokens = Math.max(requestMaxTokens, thinkingBudget + 1000);
// According to the Anthropic API, we need to use 'enabled' as the type
requestBody.thinking = {
type: "enabled",
budget_tokens: thinkingBudget
};
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
"x-api-key": this._config.apiKey
},
body: JSON.stringify(requestBody),
onNotOkResponse: async (res, decision) => {
if (res.status === 401) {
// We don't retry if the API key is invalid.
decision.retry = false;
throw new Error("API key is invalid. Please check your API key and try again.");
}
if (res.status === 400) {
const data = await res.text();
// We don't retry if the model is invalid.
decision.retry = false;
throw new Error(data);
}
return decision;
},
})
.catch((err) => {
throw new Error(err);
});
await processResponseStream(response, onData);
return result;
}
}
//# sourceMappingURL=anthropic-lang.js.map