@mymediset/sap-ai-provider
Version:
SAP AI Core provider for AI SDK
689 lines (683 loc) • 21.5 kB
JavaScript
// src/sap-ai-provider.ts
import { loadApiKey, withoutTrailingSlash } from "@ai-sdk/provider-utils";
// src/sap-ai-chat-language-model.ts
import {
combineHeaders,
createEventSourceResponseHandler,
createJsonResponseHandler,
postJsonToApi
} from "@ai-sdk/provider-utils";
// 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": {
messages.push({
role: "system",
content: message.content
});
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,${part.data}`;
contentParts.push({
type: "image_url",
image_url: {
url: imageUrl
}
});
break;
}
default: {
throw new UnsupportedFunctionalityError({
functionality: `Content type ${part.type}`
});
}
}
}
if (contentParts.length === 1 && contentParts[0].type === "text") {
messages.push({
role: "user",
content: contentParts[0].text
});
} else {
messages.push({
role: "user",
content: contentParts
});
}
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;
}
}
}
messages.push({
role: "assistant",
content: text,
tool_calls: toolCalls.length > 0 ? toolCalls : void 0
});
break;
}
case "tool": {
for (const part of message.content) {
if (part.type === "tool-result") {
messages.push({
role: "tool",
tool_call_id: part.toolCallId,
content: JSON.stringify(part.output)
});
}
}
break;
}
default: {
const _exhaustiveCheck = message;
throw new Error(`Unsupported role: ${_exhaustiveCheck}`);
}
}
}
return messages;
}
// src/sap-ai-error.ts
import { createJsonErrorResponseHandler } from "@ai-sdk/provider-utils";
import { z } from "zod";
var sapAIErrorSchema = z.object({
/** Unique identifier for tracking the request */
request_id: z.string().optional(),
/** HTTP status code or custom error code */
code: z.number().optional(),
/** Human-readable error message */
message: z.string().optional(),
/** Where the error occurred (e.g., endpoint, function) */
location: z.string().optional(),
/** Detailed error information */
error: z.object({
/** Specific error message */
message: z.string().optional(),
/** Error type code */
code: z.string().optional(),
/** Parameter that caused the error */
param: z.string().optional(),
/** Error category (e.g., 'validation', 'auth') */
type: z.string().optional()
}).optional(),
/** Additional error context or debugging information */
details: z.string().optional()
}).optional();
var SAPAIError = class extends Error {
constructor(message, data, response) {
super(message);
this.data = data;
this.response = response;
this.name = "SAPAIError";
if (data) {
this.code = data.code;
this.location = data.location;
this.requestId = data.request_id;
this.details = data.details;
}
}
};
var sapAIFailedResponseHandler = createJsonErrorResponseHandler({
errorSchema: sapAIErrorSchema,
errorToMessage: (data) => {
return data?.error?.message || data?.message || "An error occurred during the SAP AI Core request.";
},
isRetryable: (response, error) => {
const status = response.status;
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
}
});
// src/types/completion-response.ts
import { z as z2 } from "zod";
var sapAIToolCallSchema = z2.object({
id: z2.string(),
type: z2.literal("function"),
function: z2.object({
name: z2.string(),
arguments: z2.string()
})
});
var sapAIContentPartSchema = z2.union([
z2.object({
type: z2.literal("text"),
text: z2.string()
}),
z2.object({
type: z2.literal("image_url"),
image_url: z2.object({
url: z2.string(),
detail: z2.string().optional()
})
})
]);
var sapAITemplatingMessageSchema = z2.object({
role: z2.enum(["assistant", "user", "system", "tool"]),
content: z2.union([z2.string(), z2.array(sapAIContentPartSchema)]),
tool_calls: z2.array(sapAIToolCallSchema).optional(),
tool_call_id: z2.string().optional()
});
var sapAIMessageSchema = z2.object({
role: z2.enum(["assistant", "user", "system", "tool"]),
content: z2.string().nullable(),
tool_calls: z2.array(sapAIToolCallSchema).optional(),
tool_call_id: z2.string().optional()
});
var sapAIChoiceSchema = z2.object({
message: sapAIMessageSchema,
finish_reason: z2.string()
});
var sapAIUsageSchema = z2.object({
prompt_tokens: z2.number(),
completion_tokens: z2.number(),
total_tokens: z2.number()
});
var sapAILLMResultSchema = z2.object({
choices: z2.array(sapAIChoiceSchema),
usage: sapAIUsageSchema,
id: z2.string().optional(),
object: z2.string().optional(),
created: z2.number().optional(),
model: z2.string().optional(),
system_fingerprint: z2.string().optional()
});
var sapAITemplatingResultSchema = z2.array(sapAITemplatingMessageSchema);
var sapAIResponseSchema = z2.object({
request_id: z2.string(),
module_results: z2.object({
llm: sapAILLMResultSchema,
templating: sapAITemplatingResultSchema
}),
orchestration_results: z2.object({
id: z2.string().optional(),
object: z2.string().optional(),
created: z2.number().optional(),
model: z2.string().optional(),
system_fingerprint: z2.string().optional(),
choices: z2.array(sapAIChoiceSchema).optional(),
usage: sapAIUsageSchema.optional()
}).optional()
});
var sapAIStreamChoiceSchema = z2.object({
delta: z2.object({
role: z2.enum(["assistant"]).optional(),
content: z2.string().optional(),
tool_calls: z2.array(sapAIToolCallSchema).optional()
}),
finish_reason: z2.string().nullish(),
index: z2.number()
});
var sapAIStreamLLMResultSchema = z2.object({
choices: z2.array(sapAIStreamChoiceSchema),
usage: sapAIUsageSchema.nullish(),
id: z2.string().optional(),
object: z2.string().optional(),
created: z2.number().optional(),
model: z2.string().optional(),
system_fingerprint: z2.string().optional()
});
var sapAIStreamResponseSchema = z2.object({
request_id: z2.string(),
module_results: z2.object({
llm: sapAIStreamLLMResultSchema.optional(),
templating: z2.array(sapAITemplatingMessageSchema).optional()
}),
orchestration_result: z2.object({
id: z2.string().optional(),
object: z2.string().optional(),
created: z2.number().optional(),
model: z2.string().optional(),
system_fingerprint: z2.string().optional(),
choices: z2.array(sapAIStreamChoiceSchema).optional(),
usage: sapAIUsageSchema.optional()
}).optional()
});
// src/sap-ai-chat-language-model.ts
var SAPAIChatLanguageModel = class {
constructor(modelId, settings, config) {
this.specificationVersion = "v2";
this.defaultObjectGenerationMode = "json";
this.supportsImageUrls = true;
this.supportsStructuredOutputs = true;
this.settings = settings;
this.config = config;
this.modelId = modelId;
}
supportsUrl(url) {
return url.protocol === "https:";
}
get supportedUrls() {
return {
"image/*": [
/^https:\/\/.*\.(?:png|jpg|jpeg|gif|webp)$/i,
/^data:image\/.*$/
]
};
}
get provider() {
return this.config.provider;
}
getArgs(options, streaming = false) {
const warnings = [];
const availableTools = options.tools;
const supportsStructuredOutputs = !this.modelId.startsWith("anthropic--") && !this.modelId.startsWith("claude-") && !this.modelId.startsWith("amazon--");
const supportsN = !this.modelId.startsWith("amazon--");
const templatingConfig = {
template: convertToSAPMessages(options.prompt),
defaults: {},
tools: availableTools?.map((tool) => {
if (tool.type === "function") {
let parameters = tool.inputSchema;
return {
type: tool.type,
function: {
name: tool.name,
description: tool.description,
parameters: parameters || {
type: "object",
properties: {},
required: []
}
}
};
} else {
warnings.push({
type: "unsupported-tool",
tool
});
return null;
}
}).filter(Boolean)
};
if (supportsStructuredOutputs && !availableTools?.length) {
templatingConfig.response_format = {
type: "json_schema",
json_schema: {
name: "chat_completion_response",
strict: true,
schema: {
type: "object",
properties: {
role: { type: "string", enum: ["assistant"] },
content: { type: "string" }
},
required: ["role", "content"],
additionalProperties: false
}
}
};
}
const args = {
orchestration_config: {
stream: streaming,
module_configurations: {
llm_module_config: {
model_name: this.modelId,
model_version: this.settings.modelVersion ?? "latest",
model_params: {
temperature: this.settings.modelParams?.temperature ?? 0.7,
max_tokens: this.settings.modelParams?.maxTokens ?? 1e3,
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
}
},
templating_module_config: templatingConfig
}
}
};
return { args, warnings };
}
async doGenerate(options) {
const { args, warnings } = this.getArgs(options);
const headers = combineHeaders(
this.config.headers(),
options.headers ?? {}
);
const { value: response } = await postJsonToApi({
url: this.config.baseURL,
headers,
body: args,
failedResponseHandler: sapAIFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
sapAIResponseSchema
),
fetch: this.config.fetch,
abortSignal: options.abortSignal
});
const firstChoice = response.module_results.llm.choices[0];
const usage = response.module_results.llm.usage;
const content = [];
if (firstChoice.message.content) {
let text;
try {
const parsed = JSON.parse(firstChoice.message.content);
text = parsed.content || firstChoice.message.content;
} catch {
text = firstChoice.message.content;
}
if (text) {
content.push({
type: "text",
text
});
}
}
if (firstChoice.message.tool_calls) {
for (const toolCall of firstChoice.message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: toolCall.function.arguments
});
}
}
return {
content,
finishReason: firstChoice.finish_reason,
usage: {
inputTokens: usage.prompt_tokens,
outputTokens: usage.completion_tokens,
totalTokens: usage.total_tokens
},
rawCall: {
rawPrompt: args,
rawSettings: {}
},
warnings
};
}
async doStream(options) {
const { args, warnings } = this.getArgs(options, true);
const body = args;
const { responseHeaders, value: response } = await postJsonToApi({
url: this.config.baseURL,
headers: combineHeaders(this.config.headers(), options.headers),
body,
failedResponseHandler: sapAIFailedResponseHandler,
successfulResponseHandler: createEventSourceResponseHandler(
sapAIStreamResponseSchema
),
abortSignal: options.abortSignal,
fetch: this.config.fetch
});
let finishReason = "unknown";
const usage = {
inputTokens: void 0,
outputTokens: void 0,
totalTokens: void 0
};
let isFirstChunk = true;
let activeText = false;
return {
stream: response.pipeThrough(
new TransformStream({
start(controller) {
controller.enqueue({ type: "stream-start", warnings });
},
transform(chunk, controller) {
if (!chunk.success) {
controller.enqueue({ type: "error", error: chunk.error });
return;
}
const value = chunk.value;
const llmResult = value.module_results.llm;
if (!llmResult) {
return;
}
if (isFirstChunk) {
isFirstChunk = false;
controller.enqueue({
type: "response-metadata",
id: llmResult.id ?? void 0,
modelId: llmResult.model ?? void 0,
timestamp: llmResult.created ? new Date(llmResult.created * 1e3) : void 0
});
}
if (llmResult.usage != null) {
usage.inputTokens = llmResult.usage.prompt_tokens;
usage.outputTokens = llmResult.usage.completion_tokens;
usage.totalTokens = llmResult.usage.total_tokens;
}
const choice = llmResult.choices[0];
const delta = choice.delta;
if (delta.content != null && delta.content.length > 0) {
let textContent;
try {
const parsed = JSON.parse(delta.content);
textContent = parsed.content || delta.content;
} catch {
textContent = delta.content;
}
if (!activeText) {
controller.enqueue({ type: "text-start", id: "0" });
activeText = true;
}
controller.enqueue({
type: "text-delta",
id: "0",
delta: textContent
});
}
if (delta.tool_calls != null) {
for (const toolCall of delta.tool_calls) {
const toolCallId = toolCall.id;
const toolName = toolCall.function.name;
const input = toolCall.function.arguments;
controller.enqueue({
type: "tool-input-start",
id: toolCallId,
toolName
});
controller.enqueue({
type: "tool-input-delta",
id: toolCallId,
delta: input
});
controller.enqueue({
type: "tool-input-end",
id: toolCallId
});
controller.enqueue({
type: "tool-call",
toolCallId,
toolName,
input
});
}
}
if (choice.finish_reason != null) {
finishReason = choice.finish_reason;
}
},
flush(controller) {
if (activeText) {
controller.enqueue({ type: "text-end", id: "0" });
}
controller.enqueue({
type: "finish",
finishReason,
usage
});
}
})
),
rawCall: { rawPrompt: body, rawSettings: {} }
};
}
};
// src/sap-ai-provider.ts
async function getOAuthToken(serviceKey, customFetch) {
const fetchFn = customFetch || fetch;
const tokenUrl = `${serviceKey.url}/oauth/token`;
const credentials = Buffer.from(
`${serviceKey.clientid}:${serviceKey.clientsecret}`
).toString("base64");
const response = await fetchFn(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${credentials}`
},
body: "grant_type=client_credentials"
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to get OAuth access token: ${response.status} ${response.statusText}
${errorText}`
);
}
const tokenData = await response.json();
return tokenData.access_token;
}
async function createSAPAIProvider(options = {}) {
let baseURL;
let authToken;
let parsedServiceKey;
if (options.serviceKey) {
if (typeof options.serviceKey === "string") {
try {
parsedServiceKey = JSON.parse(options.serviceKey);
} catch (error) {
throw new Error("Invalid service key JSON format");
}
} else {
parsedServiceKey = options.serviceKey;
}
}
if (parsedServiceKey) {
baseURL = withoutTrailingSlash(options.baseURL) ?? `${parsedServiceKey.serviceurls.AI_API_URL}/v2`;
} else {
baseURL = withoutTrailingSlash(options.baseURL) ?? "https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com/v2";
}
if (options.token) {
authToken = options.token;
} else if (parsedServiceKey) {
authToken = await getOAuthToken(parsedServiceKey, options.fetch);
} else {
authToken = loadApiKey({
apiKey: void 0,
environmentVariableName: "SAP_AI_TOKEN",
description: "SAP AI Core"
});
}
const deploymentId = options.deploymentId ?? "d65d81e7c077e583";
const resourceGroup = options.resourceGroup ?? "default";
const createModel = (modelId, settings = {}) => {
return new SAPAIChatLanguageModel(modelId, settings, {
provider: "sap-ai",
baseURL: `${baseURL}/inference/deployments/${deploymentId}/completion`,
headers: () => ({
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"ai-resource-group": resourceGroup,
...options.headers
}),
fetch: options.fetch
});
};
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;
}
function createSAPAIProviderSync(options) {
const baseURL = withoutTrailingSlash(options.baseURL) ?? "https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com/v2";
const deploymentId = options.deploymentId ?? "d65d81e7c077e583";
const resourceGroup = options.resourceGroup ?? "default";
const createModel = (modelId, settings = {}) => {
return new SAPAIChatLanguageModel(modelId, settings, {
provider: "sap-ai",
baseURL: `${baseURL}/inference/deployments/${deploymentId}/completion`,
headers: () => ({
Authorization: `Bearer ${options.token}`,
"Content-Type": "application/json",
"ai-resource-group": resourceGroup,
...options.headers
}),
fetch: options.fetch
});
};
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;
}
function createDefaultSAPAI() {
const createModel = (modelId, settings) => {
const token = process.env.SAP_AI_TOKEN;
if (!token) {
throw new Error(
'SAP_AI_TOKEN environment variable is required for default instance. Either set SAP_AI_TOKEN or use createSAPAIProvider({ serviceKey: "..." }) instead.'
);
}
const provider2 = createSAPAIProviderSync({ token });
return provider2(modelId, settings);
};
const provider = function(modelId, settings) {
return createModel(modelId, settings);
};
provider.chat = createModel;
return provider;
}
var sapai = createDefaultSAPAI();
export {
SAPAIError,
createSAPAIProvider,
sapai
};
//# sourceMappingURL=index.mjs.map