UNPKG

@mymediset/sap-ai-provider

Version:

SAP AI Core provider for AI SDK (powered by @sap-ai-sdk/orchestration)

703 lines (696 loc) 22.1 kB
// src/sap-ai-chat-language-model.ts import { OrchestrationClient } from "@sap-ai-sdk/orchestration"; import { zodToJsonSchema } from "zod-to-json-schema"; // src/convert-to-sap-messages.ts import { UnsupportedFunctionalityError } from "@ai-sdk/provider"; function convertToSAPMessages(prompt) { const messages = []; for (const message of prompt) { switch (message.role) { case "system": { const systemMessage = { role: "system", content: message.content }; messages.push(systemMessage); break; } case "user": { const contentParts = []; for (const part of message.content) { switch (part.type) { case "text": { contentParts.push({ type: "text", text: part.text }); break; } case "file": { if (!part.mediaType.startsWith("image/")) { throw new UnsupportedFunctionalityError({ functionality: "Only image files are supported" }); } const imageUrl = part.data instanceof URL ? part.data.toString() : `data:${part.mediaType};base64,${String(part.data)}`; contentParts.push({ type: "image_url", image_url: { url: imageUrl } }); break; } default: { throw new UnsupportedFunctionalityError({ functionality: `Content type ${part.type}` }); } } } const userMessage = contentParts.length === 1 && contentParts[0].type === "text" ? { role: "user", content: contentParts[0].text ?? "" } : { role: "user", content: contentParts }; messages.push(userMessage); break; } case "assistant": { let text = ""; const toolCalls = []; for (const part of message.content) { switch (part.type) { case "text": { text += part.text; break; } case "tool-call": { toolCalls.push({ id: part.toolCallId, type: "function", function: { name: part.toolName, arguments: JSON.stringify(part.input) } }); break; } } } const assistantMessage = { role: "assistant", content: text || "", tool_calls: toolCalls.length > 0 ? toolCalls : void 0 }; messages.push(assistantMessage); break; } case "tool": { for (const part of message.content) { const toolMessage = { role: "tool", tool_call_id: part.toolCallId, content: JSON.stringify(part.output) }; messages.push(toolMessage); } break; } default: { const _exhaustiveCheck = message; throw new Error( `Unsupported role: ${_exhaustiveCheck.role}` ); } } } return messages; } // src/sap-ai-chat-language-model.ts function isZodSchema(obj) { return obj !== null && typeof obj === "object" && "_def" in obj && "parse" in obj && typeof obj.parse === "function"; } var SAPAIChatLanguageModel = class { /** AI SDK specification version */ specificationVersion = "v2"; /** Default object generation mode */ defaultObjectGenerationMode = "json"; /** Whether the model supports image URLs */ supportsImageUrls = true; /** The model identifier (e.g., 'gpt-4o', 'anthropic--claude-3.5-sonnet') */ modelId; /** Whether the model supports structured outputs */ supportsStructuredOutputs = true; /** Internal configuration */ config; /** Model-specific settings */ settings; /** * Creates a new SAP AI Chat Language Model instance. * * @param modelId - The model identifier * @param settings - Model-specific configuration settings * @param config - Internal configuration (deployment config, destination, etc.) * * @internal This constructor is not meant to be called directly. * Use the provider function instead. */ constructor(modelId, settings, config) { this.settings = settings; this.config = config; this.modelId = modelId; } /** * Checks if a URL is supported for file/image uploads. * * @param url - The URL to check * @returns True if the URL protocol is HTTPS */ supportsUrl(url) { return url.protocol === "https:"; } /** * Returns supported URL patterns for different content types. * * @returns Record of content types to regex patterns */ get supportedUrls() { return { "image/*": [ /^https:\/\/.*\.(?:png|jpg|jpeg|gif|webp)$/i, /^data:image\/.*$/ ] }; } /** * Gets the provider identifier. * * @returns The provider name ('sap-ai') */ get provider() { return this.config.provider; } /** * Builds orchestration module config for SAP AI SDK. * * @param options - Call options from the AI SDK * @returns Object containing orchestration config and warnings * * @internal */ buildOrchestrationConfig(options) { const warnings = []; const messages = convertToSAPMessages(options.prompt); let tools; if (this.settings.tools && this.settings.tools.length > 0) { tools = this.settings.tools; } else { const availableTools = options.tools; tools = availableTools?.map((tool) => { if (tool.type === "function") { const inputSchema = tool.inputSchema; const toolWithParams = tool; let parameters; if (toolWithParams.parameters && isZodSchema(toolWithParams.parameters)) { const jsonSchema = zodToJsonSchema(toolWithParams.parameters, { $refStrategy: "none" }); delete jsonSchema.$schema; parameters = { type: "object", ...jsonSchema }; } else if (inputSchema && Object.keys(inputSchema).length > 0) { const hasProperties = inputSchema.properties && typeof inputSchema.properties === "object" && Object.keys(inputSchema.properties).length > 0; if (hasProperties) { parameters = { type: "object", ...inputSchema }; } else { parameters = { type: "object", properties: {}, required: [] }; } } else { parameters = { type: "object", properties: {}, required: [] }; } return { type: "function", function: { name: tool.name, description: tool.description, parameters } }; } else { warnings.push({ type: "unsupported-tool", tool }); return null; } }).filter((t) => t !== null); } const supportsN = !this.modelId.startsWith("amazon--") && !this.modelId.startsWith("anthropic--"); const orchestrationConfig = { promptTemplating: { model: { name: this.modelId, version: this.settings.modelVersion ?? "latest", params: { max_tokens: this.settings.modelParams?.maxTokens, temperature: this.settings.modelParams?.temperature, top_p: this.settings.modelParams?.topP, frequency_penalty: this.settings.modelParams?.frequencyPenalty, presence_penalty: this.settings.modelParams?.presencePenalty, n: supportsN ? this.settings.modelParams?.n ?? 1 : void 0 } }, prompt: { template: [], tools: tools && tools.length > 0 ? tools : void 0 } }, // Include masking module if provided ...this.settings.masking ? { masking: this.settings.masking } : {}, // Include filtering module if provided ...this.settings.filtering ? { filtering: this.settings.filtering } : {} }; return { orchestrationConfig, messages, warnings }; } /** * Creates an OrchestrationClient instance. * * @param config - Orchestration module configuration * @returns OrchestrationClient instance * * @internal */ createClient(config) { return new OrchestrationClient( config, this.config.deploymentConfig, this.config.destination ); } /** * Generates a single completion (non-streaming). * * This method implements the `LanguageModelV2.doGenerate` interface, * sending a request to SAP AI Core and returning the complete response. * * **Features:** * - Tool calling support * - Multi-modal input (text + images) * - Data masking (if configured) * - Content filtering (if configured) * * @param options - Generation options including prompt, tools, and settings * @returns Promise resolving to the generation result with content, usage, and metadata * * @example * ```typescript * const result = await model.doGenerate({ * prompt: [ * { role: 'user', content: [{ type: 'text', text: 'Hello!' }] } * ] * }); * * console.log(result.content); // Generated content * console.log(result.usage); // Token usage * ``` */ async doGenerate(options) { const { orchestrationConfig, messages, warnings } = this.buildOrchestrationConfig(options); const client = this.createClient(orchestrationConfig); const response = await client.chatCompletion({ messages }); const content = []; const textContent = response.getContent(); if (textContent) { content.push({ type: "text", text: textContent }); } const toolCalls = response.getToolCalls(); if (toolCalls) { for (const toolCall of toolCalls) { content.push({ type: "tool-call", toolCallId: toolCall.id, toolName: toolCall.function.name, // AI SDK expects input as a JSON string, which it parses internally input: toolCall.function.arguments }); } } const tokenUsage = response.getTokenUsage(); const finishReasonRaw = response.getFinishReason(); const finishReason = mapFinishReason(finishReasonRaw); return { content, finishReason, usage: { inputTokens: tokenUsage.prompt_tokens, outputTokens: tokenUsage.completion_tokens, totalTokens: tokenUsage.total_tokens }, rawCall: { rawPrompt: { config: orchestrationConfig, messages }, rawSettings: {} }, warnings }; } /** * Generates a streaming completion. * * This method implements the `LanguageModelV2.doStream` interface, * sending a streaming request to SAP AI Core and returning a stream of response parts. * * **Stream Events:** * - `stream-start` - Stream initialization * - `response-metadata` - Response metadata (model, timestamp) * - `text-start` - Text generation starts * - `text-delta` - Incremental text chunks * - `text-end` - Text generation completes * - `tool-call` - Tool call detected * - `finish` - Stream completes with usage and finish reason * - `error` - Error occurred * * @param options - Streaming options including prompt, tools, and settings * @returns Promise resolving to stream and raw call metadata * * @example * ```typescript * const { stream } = await model.doStream({ * prompt: [ * { role: 'user', content: [{ type: 'text', text: 'Write a story' }] } * ] * }); * * for await (const part of stream) { * if (part.type === 'text-delta') { * process.stdout.write(part.delta); * } * } * ``` */ async doStream(options) { const { orchestrationConfig, messages, warnings } = this.buildOrchestrationConfig(options); const client = this.createClient(orchestrationConfig); const streamResponse = await client.stream( { messages }, options.abortSignal, { promptTemplating: { include_usage: true } } ); let finishReason = "unknown"; const usage = { inputTokens: void 0, outputTokens: void 0, totalTokens: void 0 }; let isFirstChunk = true; let activeText = false; const toolCallsInProgress = /* @__PURE__ */ new Map(); const sdkStream = streamResponse.stream; const transformedStream = new ReadableStream({ async start(controller) { controller.enqueue({ type: "stream-start", warnings }); try { for await (const chunk of sdkStream) { if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: "response-metadata", id: void 0, modelId: void 0, timestamp: /* @__PURE__ */ new Date() }); } const deltaContent = chunk.getDeltaContent(); if (deltaContent) { if (!activeText) { controller.enqueue({ type: "text-start", id: "0" }); activeText = true; } controller.enqueue({ type: "text-delta", id: "0", delta: deltaContent }); } const deltaToolCalls = chunk.getDeltaToolCalls(); if (deltaToolCalls) { for (const toolCallChunk of deltaToolCalls) { const index = toolCallChunk.index; if (!toolCallsInProgress.has(index)) { toolCallsInProgress.set(index, { id: toolCallChunk.id ?? `tool_${String(index)}`, name: toolCallChunk.function?.name ?? "", arguments: "" }); const tc2 = toolCallsInProgress.get(index); if (!tc2) continue; if (toolCallChunk.function?.name) { controller.enqueue({ type: "tool-input-start", id: tc2.id, toolName: tc2.name }); } } const tc = toolCallsInProgress.get(index); if (!tc) continue; if (toolCallChunk.id) { tc.id = toolCallChunk.id; } if (toolCallChunk.function?.name) { tc.name = toolCallChunk.function.name; } if (toolCallChunk.function?.arguments) { tc.arguments += toolCallChunk.function.arguments; controller.enqueue({ type: "tool-input-delta", id: tc.id, delta: toolCallChunk.function.arguments }); } } } const chunkFinishReason = chunk.getFinishReason(); if (chunkFinishReason) { finishReason = mapFinishReason(chunkFinishReason); } const chunkUsage = chunk.getTokenUsage(); if (chunkUsage) { usage.inputTokens = chunkUsage.prompt_tokens; usage.outputTokens = chunkUsage.completion_tokens; usage.totalTokens = chunkUsage.total_tokens; } } const toolCalls = Array.from(toolCallsInProgress.values()); for (const tc of toolCalls) { controller.enqueue({ type: "tool-input-end", id: tc.id }); controller.enqueue({ type: "tool-call", toolCallId: tc.id, toolName: tc.name, input: tc.arguments }); } if (activeText) { controller.enqueue({ type: "text-end", id: "0" }); } const finalUsage = streamResponse.getTokenUsage(); if (finalUsage) { usage.inputTokens = finalUsage.prompt_tokens; usage.outputTokens = finalUsage.completion_tokens; usage.totalTokens = finalUsage.total_tokens; } const finalFinishReason = streamResponse.getFinishReason(); if (finalFinishReason) { finishReason = mapFinishReason(finalFinishReason); } controller.enqueue({ type: "finish", finishReason, usage }); controller.close(); } catch (error) { controller.enqueue({ type: "error", error: error instanceof Error ? error : new Error(String(error)) }); controller.close(); } } }); return { stream: transformedStream, rawCall: { rawPrompt: { config: orchestrationConfig, messages }, rawSettings: {} } }; } }; function mapFinishReason(reason) { if (!reason) return "unknown"; switch (reason.toLowerCase()) { case "stop": return "stop"; case "length": return "length"; case "tool_calls": case "function_call": return "tool-calls"; case "content_filter": return "content-filter"; default: return "unknown"; } } // src/sap-ai-provider.ts function createSAPAIProvider(options = {}) { const resourceGroup = options.resourceGroup ?? "default"; const deploymentConfig = options.deploymentId ? { deploymentId: options.deploymentId } : { resourceGroup }; const createModel = (modelId, settings = {}) => { const mergedSettings = { ...options.defaultSettings, ...settings, modelParams: { ...options.defaultSettings?.modelParams ?? {}, ...settings.modelParams ?? {} } }; return new SAPAIChatLanguageModel(modelId, mergedSettings, { provider: "sap-ai", deploymentConfig, destination: options.destination }); }; const provider = function(modelId, settings) { if (new.target) { throw new Error( "The SAP AI provider function cannot be called with the new keyword." ); } return createModel(modelId, settings); }; provider.chat = createModel; return provider; } var sapai = createSAPAIProvider(); // src/sap-ai-chat-settings.ts import { buildDpiMaskingProvider, buildAzureContentSafetyFilter, buildLlamaGuard38BFilter, buildDocumentGroundingConfig, buildTranslationConfig } from "@sap-ai-sdk/orchestration"; // src/sap-ai-error.ts var SAPAIError = class _SAPAIError extends Error { /** HTTP status code or custom error code */ code; /** Where the error occurred (e.g., module name) */ location; /** Unique identifier for tracking the request */ requestId; /** Additional error context or debugging information */ details; /** Original cause of the error */ cause; constructor(message, options) { super(message); this.name = "SAPAIError"; this.code = options?.code; this.location = options?.location; this.requestId = options?.requestId; this.details = options?.details; this.cause = options?.cause; } /** * Creates a SAPAIError from an OrchestrationErrorResponse. * * @param errorResponse - The error response from SAP AI SDK * @returns A new SAPAIError instance */ static fromOrchestrationError(errorResponse) { const error = errorResponse.error; if (Array.isArray(error)) { const firstError = error[0]; return new _SAPAIError( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition firstError?.message ?? "Unknown orchestration error", { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition code: firstError?.code, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition location: firstError?.location, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition requestId: firstError?.request_id } ); } else { return new _SAPAIError(error.message ?? "Unknown orchestration error", { code: error.code, location: error.location, requestId: error.request_id }); } } /** * Creates a SAPAIError from a generic error. * * @param error - The original error * @param context - Optional context about where the error occurred * @returns A new SAPAIError instance */ static fromError(error, context) { if (error instanceof _SAPAIError) { return error; } let message; if (error instanceof Error) { message = error.message; } else if (error == null) { message = "Unknown error"; } else if (typeof error === "string" || typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") { message = String(error); } else { try { message = JSON.stringify(error); } catch { message = "[Unstringifiable Value]"; } } return new _SAPAIError(context ? `${context}: ${message}` : message, { cause: error }); } }; // src/types/completion-response.ts import { OrchestrationResponse, OrchestrationStreamResponse, OrchestrationStreamChunkResponse } from "@sap-ai-sdk/orchestration"; // src/index.ts import { OrchestrationClient as OrchestrationClient2 } from "@sap-ai-sdk/orchestration"; export { OrchestrationClient2 as OrchestrationClient, OrchestrationResponse, OrchestrationStreamChunkResponse, OrchestrationStreamResponse, SAPAIError, buildAzureContentSafetyFilter, buildDocumentGroundingConfig, buildDpiMaskingProvider, buildLlamaGuard38BFilter, buildTranslationConfig, createSAPAIProvider, sapai }; //# sourceMappingURL=index.js.map