aiwrapper
Version:
A Universal AI Wrapper for JavaScript & TypeScript
262 lines • 12.1 kB
JavaScript
import { OpenAILikeLang } from "../openai-like/openai-like-lang.js";
import { models } from 'aimodels';
import { LangResultWithMessages } from "../language-provider.js";
export class GroqLang extends OpenAILikeLang {
constructor(options) {
const modelName = options.model || "llama3-70b-8192";
super({
apiKey: options.apiKey,
model: modelName,
systemPrompt: options.systemPrompt || "",
baseURL: "https://api.groq.com/openai/v1",
bodyProperties: options.bodyProperties || {},
maxTokens: options.maxTokens,
});
}
async chat(messages, onResult) {
// Initialize the result
const result = new LangResultWithMessages(messages);
const transformedMessages = this.transformMessages(messages);
// Get the model info and check if it can reason
const modelInfo = models.id(this._config.model);
const isReasoningModel = (modelInfo === null || modelInfo === void 0 ? void 0 : modelInfo.canReason()) || false;
const bodyProperties = Object.assign({}, this._config.bodyProperties);
// Only add reasoning_format for reasoning models
if (isReasoningModel && !bodyProperties.reasoning_format) {
bodyProperties.reasoning_format = "parsed";
}
// For non-streaming calls
if (!onResult) {
// Make a direct API call
const body = Object.assign(Object.assign({}, this.transformBody({
model: this._config.model,
messages: transformedMessages,
stream: false,
max_tokens: this._config.maxTokens || 4000,
})), bodyProperties);
const response = await fetch(`${this._config.baseURL}/chat/completions`, {
method: "POST",
headers: Object.assign(Object.assign({ "Content-Type": "application/json" }, (this._config.apiKey ? { "Authorization": `Bearer ${this._config.apiKey}` } : {})), this._config.headers),
body: JSON.stringify(body),
});
const data = await response.json();
if (data.choices && data.choices.length > 0) {
const message = data.choices[0].message;
// Handle parsed format reasoning and content
if (message.reasoning) {
result.thinking = message.reasoning;
result.answer = message.content || "";
}
else {
// Handle raw format if that was requested
result.answer = message.content || "";
// Extract thinking from raw format
// Do this even if model isn't identified as a reasoning model
// to handle cases where our model data is outdated
const thinkingContent = this.extractThinking(result.answer);
if (thinkingContent.thinking) {
result.thinking = thinkingContent.thinking;
result.answer = thinkingContent.answer;
}
}
// Add to messages
result.messages = [...messages, {
role: "assistant",
content: result.answer,
}];
}
return result;
}
// For streaming
let thinkingContent = "";
let visibleContent = "";
// Variables to track streaming state for thinking extraction
let openThinkTagIndex = -1;
let pendingThinkingContent = "";
const onData = (data) => {
if (data.finished) {
// When streaming is complete, do one final extraction
// regardless of model reasoning capability in our database
const extracted = this.extractThinking(visibleContent);
if (extracted.thinking) {
result.thinking = extracted.thinking;
result.answer = extracted.answer;
}
result.finished = true;
onResult === null || onResult === void 0 ? void 0 : onResult(result);
return;
}
if (data.choices !== undefined) {
const delta = data.choices[0].delta || {};
// For parsed reasoning format
if (delta.reasoning) {
thinkingContent += delta.reasoning;
result.thinking = thinkingContent;
}
// Handle content
if (delta.content) {
const currentChunk = delta.content;
visibleContent += currentChunk;
// Process the chunk for potential thinking content
this.processChunkForThinking(visibleContent, result);
// Update tracking variables based on current state
openThinkTagIndex = visibleContent.lastIndexOf("<think>");
if (openThinkTagIndex !== -1) {
const closeTagIndex = visibleContent.indexOf("</think>", openThinkTagIndex);
if (closeTagIndex === -1) {
// We have an open tag but no close tag yet
pendingThinkingContent = visibleContent.substring(openThinkTagIndex + 7); // +7 to skip "<think>"
}
}
}
// Update messages
result.messages = [...messages, {
role: "assistant",
content: result.answer,
}];
onResult === null || onResult === void 0 ? void 0 : onResult(result);
}
};
// Call the API with streaming
const streamingBody = Object.assign(Object.assign({}, this.transformBody({
model: this._config.model,
messages: transformedMessages,
stream: true,
max_tokens: this._config.maxTokens || 4000,
})), bodyProperties);
await this.callAPI("/chat/completions", streamingBody, onData);
return result;
}
// Simple helper to extract thinking content from raw format
extractThinking(content) {
const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
const matches = content.match(thinkRegex);
if (!matches || matches.length === 0) {
return { thinking: "", answer: content };
}
// Extract thinking content
const thinking = matches
.map((match) => match.replace(/<think>|<\/think>/g, "").trim())
.join("\n");
// Remove thinking tags for clean answer
const answer = content.replace(thinkRegex, "").trim();
return { thinking, answer };
}
// Process a chunk for thinking content during streaming
processChunkForThinking(fullContent, result) {
// Check if we have a complete thinking section
const extracted = this.extractThinking(fullContent);
if (extracted.thinking) {
// We have one or more complete thinking sections
result.thinking = extracted.thinking;
result.answer = extracted.answer;
return;
}
// Check for partial thinking tags
if (fullContent.includes("<think>")) {
// We have at least an opening tag
const lastOpenTagIndex = fullContent.lastIndexOf("<think>");
const firstCloseTagIndex = fullContent.indexOf("</think>");
if (firstCloseTagIndex === -1 || lastOpenTagIndex > firstCloseTagIndex) {
// We have an open tag without a closing tag
// Everything from the open tag to the end should be considered thinking
const beforeThinkingContent = fullContent.substring(0, lastOpenTagIndex).trim();
const potentialThinkingContent = fullContent.substring(lastOpenTagIndex + 7).trim();
result.thinking = potentialThinkingContent;
result.answer = beforeThinkingContent;
return;
}
// If we have both tags but the regex didn't match (shouldn't happen but just in case)
// Extract the content manually
const startIndex = fullContent.indexOf("<think>") + 7;
const endIndex = fullContent.indexOf("</think>");
if (startIndex < endIndex) {
const thinkingContent = fullContent.substring(startIndex, endIndex).trim();
const beforeThinking = fullContent.substring(0, fullContent.indexOf("<think>")).trim();
const afterThinking = fullContent.substring(fullContent.indexOf("</think>") + 8).trim();
result.thinking = thinkingContent;
result.answer = (beforeThinking + " " + afterThinking).trim();
}
}
else {
// No thinking tags yet, just update the answer
result.answer = fullContent;
}
}
// Helper method to call the API
async callAPI(endpoint, body, onData) {
const response = await fetch(`${this._config.baseURL}${endpoint}`, {
method: "POST",
headers: Object.assign(Object.assign({ "Content-Type": "application/json" }, (this._config.apiKey ? { "Authorization": `Bearer ${this._config.apiKey}` } : {})), this._config.headers),
body: JSON.stringify(body),
}).catch((err) => {
throw new Error(err);
});
await this.processResponse(response, onData);
return response;
}
// Process the response stream
async processResponse(response, onData) {
var _a;
const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader();
if (!reader)
return;
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
// Process complete lines
let lineEnd = buffer.indexOf('\n');
while (lineEnd !== -1) {
const line = buffer.substring(0, lineEnd).trim();
buffer = buffer.substring(lineEnd + 1);
if (line.startsWith('data: ')) {
const dataValue = line.substring(6);
if (dataValue === '[DONE]') {
onData({ finished: true });
}
else {
try {
const data = JSON.parse(dataValue);
onData(data);
}
catch (e) {
console.error("Error parsing JSON:", e);
}
}
}
lineEnd = buffer.indexOf('\n');
}
}
// Process any remaining buffer content
if (buffer.trim() && buffer.startsWith('data: ')) {
const dataValue = buffer.substring(6).trim();
if (dataValue === '[DONE]') {
onData({ finished: true });
}
else if (dataValue) {
try {
const data = JSON.parse(dataValue);
onData(data);
}
catch (e) {
console.error("Error parsing JSON:", e);
}
}
}
onData({ finished: true });
}
catch (e) {
console.error("Error processing response stream:", e);
throw e;
}
finally {
reader.releaseLock();
}
}
}
//# sourceMappingURL=groq-lang.js.map