@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
JavaScript
// 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