Skip to main content
Glama
ai-sdk-model.ts11.3 kB
import { getModelContextLength, initializeAIModel } from "@superglue/shared/utils"; import { AssistantModelMessage, TextPart, ToolCallPart, ToolResultPart, Tool, generateText, jsonSchema, tool } from "ai"; import { server_defaults } from "../default.js"; import { LLMToolDefinition, logToolExecution } from "./llm-tool-utils.js"; import { logMessage } from "../utils/logs.js"; import { LLM, LLMMessage, LLMObjectGeneratorInput, LLMObjectResponse, LLMResponse, LLMToolWithContext } from "./llm-base-model.js"; function isProviderError(error: any): boolean { const statusCode = error?.statusCode || error?.status; if (statusCode >= 500 || statusCode === 429 || statusCode === 503) return true; const errorMsg = (error?.message || '').toLowerCase(); const providerErrorPatterns = [ 'overloaded', 'service unavailable', 'timeout', 'econnrefused', 'etimedout', 'rate limit', 'temporarily unavailable', 'internal server error', '503', '500', '502', '504' ]; return providerErrorPatterns.some(pattern => errorMsg.includes(pattern)); } export class AiSdkModel implements LLM { public contextLength: number; private model: any; private modelId: string; private fallbackModel: any | null; constructor(modelId?: string) { this.modelId = modelId || 'claude-sonnet-4-5'; this.model = initializeAIModel({ providerEnvVar: 'LLM_PROVIDER', defaultModel: this.modelId }); this.contextLength = getModelContextLength(this.modelId); this.fallbackModel = process.env.LLM_FALLBACK_PROVIDER ? initializeAIModel({ providerEnvVar: 'LLM_FALLBACK_PROVIDER', defaultModel: this.modelId }) : null; } private getDateMessage(): LLMMessage { return { role: "system" as const, content: "The current date and time is " + new Date().toISOString() } as LLMMessage; } private buildTools( schemaObj: any, tools?: LLMToolWithContext[] ): Record<string, Tool> { const defaultTools: Record<string, Tool> = { submit: tool({ description: "Submit the final result in the required format. Submit the result even if it's an error and keep submitting until we stop. Keep non-function messages short and concise because they are only for debugging.", inputSchema: schemaObj, }), abort: tool({ description: "There is absolutely no way given the input to complete the request successfully, abort the request", inputSchema: jsonSchema({ type: "object", properties: { reason: { type: "string", description: "The reason for aborting" } }, required: ["reason"] }), }), }; if (tools && tools.length > 0) { for (const item of tools) { const { toolDefinition: toolDef, toolContext: toolContext } = item; const isCustomTool = 'name' in toolDef && 'arguments' in toolDef && 'description' in toolDef; if (isCustomTool) { const toolDef = item.toolDefinition as LLMToolDefinition; defaultTools[toolDef.name] = tool({ description: toolDef.description, inputSchema: jsonSchema(toolDef.arguments), execute: toolDef.execute ? async (args) => { return await toolDef.execute!(args, toolContext); } : undefined, }); } else { Object.assign(defaultTools, toolDef); } } } return defaultTools; } private cleanSchema(schema: any, isRoot: boolean = true): any { if (!schema || typeof schema !== 'object') return schema; const cleaned = { ...schema }; // Normalize object/array schemas if (cleaned.type === 'object' || cleaned.type === 'array') { cleaned.additionalProperties = false; cleaned.strict = true; delete cleaned.patternProperties; if (cleaned.properties) { for (const key in cleaned.properties) { cleaned.properties[key] = this.cleanSchema(cleaned.properties[key], false); } } if (cleaned.items) { cleaned.items = this.cleanSchema(cleaned.items, false); delete cleaned.minItems; delete cleaned.maxItems; } } // Anthropic tool input must be an object at the root. If the root // schema is an array, wrap it into an object under `result`. if (isRoot && cleaned.type === 'array') { const arraySchema = this.cleanSchema(cleaned, false); return { type: 'object', properties: { result: arraySchema, }, required: ['result'], additionalProperties: false, strict: true, }; } return cleaned; } private async generateTextWithFallback(params: { model: any; messages: LLMMessage[]; temperature?: number; tools?: Record<string, Tool>; toolChoice?: 'auto' | 'required' | 'none' | { type: 'tool'; toolName: string }; }): Promise<any> { try { return await generateText({ model: params.model, messages: params.messages, temperature: params.temperature, tools: params.tools, toolChoice: params.toolChoice, maxRetries: server_defaults.LLM.MAX_INTERNAL_RETRIES, }); } catch (error) { if (this.fallbackModel && isProviderError(error)) { logMessage('warn', `LLM provider failed with message: (${error.message}), trying fallback provider`); return await generateText({ model: this.fallbackModel, messages: params.messages, temperature: params.temperature, tools: params.tools, toolChoice: params.toolChoice, maxRetries: server_defaults.LLM.MAX_INTERNAL_RETRIES, }); } throw error; } } async generateText(messages: LLMMessage[], temperature: number = 0): Promise<LLMResponse> { const dateMessage = this.getDateMessage(); messages = [dateMessage, ...messages] as LLMMessage[]; const result = await this.generateTextWithFallback({ model: this.model, messages: messages, temperature, }); const updatedMessages = [...messages, { role: "assistant" as const, content: result.text } as LLMMessage]; return { response: result.text, messages: updatedMessages }; } /** This function is used to generate an object response from the language model. This is done by calling the generateText function together with a submit tool that has the input schema of our desired output object. We set the tool choice to required so that the LLM is forced to call a tool. When the LLM returns, we check for the submit tool call and return the result. If the LLM does not return a submit tool call, we try again. */ async generateObject<T>( input: LLMObjectGeneratorInput ): Promise<LLMObjectResponse<T>> { const dateMessage = this.getDateMessage(); // Clean schema: remove patternProperties, minItems/maxItems, set strict/additionalProperties const schema = this.cleanSchema(input.schema); // Handle O-model temperature let temperatureToUse: number | undefined = input.temperature; if (this.modelId.startsWith('o')) { temperatureToUse = undefined; } const schemaObj = jsonSchema(schema); const availableTools = this.buildTools(schemaObj, input.tools); let conversationMessages: LLMMessage[] = String(input.messages[0]?.content)?.startsWith("The current date and time is") ? input.messages : [dateMessage, ...input.messages]; try { let finalResult: any = null; while (finalResult === null) { const result = await this.generateTextWithFallback({ model: this.model, messages: conversationMessages, tools: availableTools, toolChoice: input.toolChoice || 'required', temperature: temperatureToUse, }); if(result.finishReason === 'error' || result.finishReason === 'content-filter' || result.finishReason === 'other') { throw new Error("Error generating LLM response: " + JSON.stringify(result.content || "no content")); } // Check for submit/abort in tool calls for (const toolCall of result.toolCalls) { if (toolCall.toolName === 'submit') { finalResult = (toolCall.input as any)?.result ?? toolCall.input; break; } if (toolCall.toolName === 'abort') { const updatedMessages = [...conversationMessages, { role: "assistant" as const, content: JSON.stringify(finalResult) }]; return { success: false, response: (toolCall.input as any)?.reason, messages: updatedMessages }; } } if (result.text.trim().length > 0) { conversationMessages.push({ role: "assistant" as const, content: [{ type: "text", text: result.text } as TextPart], } as LLMMessage); } for (const toolCall of result.toolCalls) { conversationMessages.push({ role: 'assistant', content: [{ type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input ?? {} } as ToolCallPart] } as AssistantModelMessage); const toolResult = result.toolResults.find(tr => tr.toolCallId === toolCall.toolCallId); if (toolResult) { logToolExecution(toolCall.toolName, toolCall.input, toolResult.output); conversationMessages.push({ role: 'tool', content: [{ type: 'tool-result', toolCallId: toolResult.toolCallId, toolName: toolResult.toolName, output: { "type": "text", "value": toolResult.output?.toString() ?? "" } } as ToolResultPart] }); } else { conversationMessages.push({ role: 'tool', content: [{ type: 'tool-result', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, output: { "type": "text", "value": "Tool did not output anything" } } as ToolResultPart] }); } } if (!finalResult && result.toolCalls.length === 0) { throw new Error("No tool calls received from the model"); } } const updatedMessages = [...conversationMessages, { role: "assistant" as const, content: JSON.stringify(finalResult) }]; return { success: true, response: finalResult, messages: updatedMessages }; } catch (error) { logMessage('error', `Error generating LLM response: ${error}`); const updatedMessages = [...input.messages, { role: "assistant" as const, content: "Error: Vercel AI API Error: " + (error as any)?.message } as LLMMessage]; return { success: false, response: "Error: Vercel AI API Error: " + (error as Error).message, messages: updatedMessages }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/superglue-ai/superglue'

If you have feedback or need assistance with the MCP directory API, please join our Discord server