@jerome-benoit/sap-ai-provider 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29968,18 +29968,29 @@ function convertToAISDKError(error, context) {
29968
29968
  if (error instanceof APICallError || error instanceof LoadAPIKeyError) {
29969
29969
  return error;
29970
29970
  }
29971
- if (isOrchestrationErrorResponse(error)) {
29972
- return convertSAPErrorToAPICallError(error, {
29971
+ const rootError = error instanceof Error && (0, import_util.isErrorWithCause)(error) ? error.rootCause : error;
29972
+ if (isOrchestrationErrorResponse(rootError)) {
29973
+ return convertSAPErrorToAPICallError(rootError, {
29973
29974
  ...context,
29974
29975
  responseHeaders: context?.responseHeaders ?? getAxiosResponseHeaders(error)
29975
29976
  });
29976
29977
  }
29978
+ if (rootError instanceof Error) {
29979
+ const parsedError = tryExtractSAPErrorFromMessage(rootError.message);
29980
+ if (parsedError && isOrchestrationErrorResponse(parsedError)) {
29981
+ return convertSAPErrorToAPICallError(parsedError, {
29982
+ ...context,
29983
+ responseHeaders: context?.responseHeaders ?? getAxiosResponseHeaders(error)
29984
+ });
29985
+ }
29986
+ }
29977
29987
  const responseHeaders = context?.responseHeaders ?? getAxiosResponseHeaders(error);
29978
- if (error instanceof Error) {
29979
- const errorMsg = error.message.toLowerCase();
29980
- if (errorMsg.includes("authentication") || errorMsg.includes("unauthorized") || errorMsg.includes("aicore_service_key") || errorMsg.includes("invalid credentials")) {
29988
+ if (rootError instanceof Error) {
29989
+ const errorMsg = rootError.message.toLowerCase();
29990
+ const originalMsg = rootError.message;
29991
+ if (errorMsg.includes("authentication") || errorMsg.includes("unauthorized") || errorMsg.includes("aicore_service_key") || errorMsg.includes("invalid credentials") || errorMsg.includes("service credentials") || errorMsg.includes("service binding")) {
29981
29992
  return new LoadAPIKeyError({
29982
- message: `SAP AI Core authentication failed: ${error.message}
29993
+ message: `SAP AI Core authentication failed: ${originalMsg}
29983
29994
 
29984
29995
  Make sure your AICORE_SERVICE_KEY environment variable is set correctly.
29985
29996
  See: https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-service-key`
@@ -29989,15 +30000,79 @@ See: https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-serv
29989
30000
  return new APICallError({
29990
30001
  cause: error,
29991
30002
  isRetryable: true,
29992
- message: `Network error connecting to SAP AI Core: ${error.message}`,
30003
+ message: `Network error connecting to SAP AI Core: ${originalMsg}`,
29993
30004
  requestBodyValues: context?.requestBody,
29994
30005
  responseHeaders,
29995
30006
  statusCode: 503,
29996
30007
  url: context?.url ?? ""
29997
30008
  });
29998
30009
  }
30010
+ if (errorMsg.includes("could not resolve destination")) {
30011
+ return new APICallError({
30012
+ cause: error,
30013
+ isRetryable: false,
30014
+ message: `SAP AI Core destination error: ${originalMsg}
30015
+
30016
+ Check your destination configuration or provide a valid destinationName.`,
30017
+ requestBodyValues: context?.requestBody,
30018
+ responseHeaders,
30019
+ statusCode: 500,
30020
+ url: context?.url ?? ""
30021
+ });
30022
+ }
30023
+ if (errorMsg.includes("failed to resolve deployment") || errorMsg.includes("no deployment matched")) {
30024
+ return new APICallError({
30025
+ cause: error,
30026
+ isRetryable: false,
30027
+ message: `SAP AI Core deployment error: ${originalMsg}
30028
+
30029
+ Make sure you have a running orchestration deployment in your AI Core instance.
30030
+ See: https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-orchestration`,
30031
+ requestBodyValues: context?.requestBody,
30032
+ responseHeaders,
30033
+ statusCode: 404,
30034
+ url: context?.url ?? ""
30035
+ });
30036
+ }
30037
+ if (errorMsg.includes("filtered by the output filter")) {
30038
+ return new APICallError({
30039
+ cause: error,
30040
+ isRetryable: false,
30041
+ message: `Content was filtered: ${originalMsg}
30042
+
30043
+ The model's response was blocked by content safety filters. Try a different prompt.`,
30044
+ requestBodyValues: context?.requestBody,
30045
+ responseHeaders,
30046
+ statusCode: 400,
30047
+ url: context?.url ?? ""
30048
+ });
30049
+ }
30050
+ const statusMatch = /status code (\d+)/i.exec(originalMsg);
30051
+ if (statusMatch) {
30052
+ const extractedStatus = parseInt(statusMatch[1], 10);
30053
+ return new APICallError({
30054
+ cause: error,
30055
+ isRetryable: isRetryable(extractedStatus),
30056
+ message: `SAP AI Core request failed: ${originalMsg}`,
30057
+ requestBodyValues: context?.requestBody,
30058
+ responseHeaders,
30059
+ statusCode: extractedStatus,
30060
+ url: context?.url ?? ""
30061
+ });
30062
+ }
30063
+ if (errorMsg.includes("iterate over") || errorMsg.includes("consumed stream") || errorMsg.includes("parse message into json") || errorMsg.includes("no body")) {
30064
+ return new APICallError({
30065
+ cause: error,
30066
+ isRetryable: true,
30067
+ message: `SAP AI Core streaming error: ${originalMsg}`,
30068
+ requestBodyValues: context?.requestBody,
30069
+ responseHeaders,
30070
+ statusCode: 500,
30071
+ url: context?.url ?? ""
30072
+ });
30073
+ }
29999
30074
  }
30000
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error occurred";
30075
+ const message = rootError instanceof Error ? rootError.message : typeof rootError === "string" ? rootError : "Unknown error occurred";
30001
30076
  const fullMessage = context?.operation ? `SAP AI Core ${context.operation} failed: ${message}` : `SAP AI Core error: ${message}`;
30002
30077
  return new APICallError({
30003
30078
  cause: error,
@@ -30058,13 +30133,35 @@ function normalizeHeaders(headers) {
30058
30133
  if (entries.length === 0) return void 0;
30059
30134
  return Object.fromEntries(entries);
30060
30135
  }
30136
+ function tryExtractSAPErrorFromMessage(message) {
30137
+ const jsonMatch = /\{[\s\S]*\}/.exec(message);
30138
+ if (!jsonMatch) return null;
30139
+ try {
30140
+ const parsed = JSON.parse(jsonMatch[0]);
30141
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
30142
+ return parsed;
30143
+ }
30144
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
30145
+ return { error: parsed };
30146
+ }
30147
+ return null;
30148
+ } catch {
30149
+ return null;
30150
+ }
30151
+ }
30061
30152
 
30062
30153
  // src/sap-ai-language-model.ts
30063
30154
  var StreamIdGenerator = class {
30155
+ /**
30156
+ * Generates a unique ID for a response stream.
30157
+ * @returns RFC 4122-compliant UUID
30158
+ */
30159
+ generateResponseId() {
30160
+ return crypto.randomUUID();
30161
+ }
30064
30162
  /**
30065
30163
  * Generates a unique ID for a text block.
30066
- *
30067
- * @returns RFC 4122-compliant UUID string
30164
+ * @returns RFC 4122-compliant UUID
30068
30165
  */
30069
30166
  generateTextBlockId() {
30070
30167
  return crypto.randomUUID();
@@ -30081,7 +30178,11 @@ var SAPAILanguageModel = class {
30081
30178
  * SAP AI Core will fail the request at runtime.
30082
30179
  */
30083
30180
  supportsImageUrls = true;
30084
- /** Multiple completions via the `n` parameter (provider-specific support). */
30181
+ /**
30182
+ * Multiple completions via the `n` parameter.
30183
+ * Note: Amazon and Anthropic models do not support this feature.
30184
+ * The provider silently omits the parameter for unsupported models.
30185
+ */
30085
30186
  supportsMultipleCompletions = true;
30086
30187
  /** Parallel tool calls. */
30087
30188
  supportsParallelToolCalls = true;
@@ -30092,52 +30193,14 @@ var SAPAILanguageModel = class {
30092
30193
  /** Tool/function calling. */
30093
30194
  supportsToolCalls = true;
30094
30195
  /**
30095
- * Generates text completion using SAP AI Core's Orchestration API.
30096
- *
30097
- * This method implements the `LanguageModelV3.doGenerate` interface,
30098
- * providing synchronous (non-streaming) text generation with support for:
30099
- * - Multi-turn conversations with system/user/assistant messages
30100
- * - Tool calling (function calling) with structured outputs
30101
- * - Multi-modal inputs (text + images)
30102
- * - Data masking via SAP DPI
30103
- * - Content filtering via Azure Content Safety or Llama Guard
30104
- *
30105
- * **Return Structure:**
30106
- * - Finish reason: `{ unified: string, raw?: string }`
30107
- * - Usage: Nested structure with token breakdown `{ inputTokens: { total, ... }, outputTokens: { total, ... } }`
30108
- * - Warnings: Array of warnings with `type` and optional `feature` field
30109
- *
30110
- * @param options - Generation options including prompt, tools, temperature, etc.
30111
- * @returns Promise resolving to generation result with content, usage, and metadata
30112
- *
30113
- * @throws {InvalidPromptError} If prompt format is invalid
30114
- * @throws {InvalidArgumentError} If arguments are malformed
30115
- * @throws {APICallError} If the SAP AI Core API call fails
30116
- *
30117
- * @example
30118
- * ```typescript
30119
- * const result = await model.doGenerate({
30120
- * prompt: [
30121
- * { role: 'user', content: [{ type: 'text', text: 'Hello!' }] }
30122
- * ],
30123
- * temperature: 0.7,
30124
- * maxTokens: 100
30125
- * });
30126
- *
30127
- * console.log(result.content); // Array of V3 content parts
30128
- * console.log(result.finishReason.unified); // "stop", "length", "tool-calls", etc.
30129
- * console.log(result.usage.inputTokens.total); // Total input tokens
30130
- * ```
30131
- *
30132
- * @since 1.0.0
30133
- * @since 4.0.0 Updated to LanguageModelV3 interface
30196
+ * Returns the provider identifier.
30197
+ * @returns The provider name
30134
30198
  */
30135
30199
  get provider() {
30136
30200
  return this.config.provider;
30137
30201
  }
30138
30202
  /**
30139
30203
  * Returns supported URL patterns for different content types.
30140
- *
30141
30204
  * @returns Record of content types to regex patterns
30142
30205
  */
30143
30206
  get supportedUrls() {
@@ -30149,13 +30212,10 @@ var SAPAILanguageModel = class {
30149
30212
  settings;
30150
30213
  /**
30151
30214
  * Creates a new SAP AI Chat Language Model instance.
30152
- *
30215
+ * @internal
30153
30216
  * @param modelId - The model identifier
30154
30217
  * @param settings - Model-specific configuration settings
30155
30218
  * @param config - Internal configuration (deployment config, destination, etc.)
30156
- *
30157
- * @internal This constructor is not meant to be called directly.
30158
- * Use the provider function instead.
30159
30219
  */
30160
30220
  constructor(modelId, settings, config) {
30161
30221
  this.settings = settings;
@@ -30177,15 +30237,12 @@ var SAPAILanguageModel = class {
30177
30237
  *
30178
30238
  * **Note on Abort Signal:**
30179
30239
  * The abort signal implementation uses Promise.race to reject the promise when
30180
- * the signal is aborted. However, this does not cancel the underlying HTTP request
30181
- * to SAP AI Core - the request continues executing on the server. This is a
30182
- * limitation of the SAP AI SDK's chatCompletion API.
30183
- *
30184
- * @see https://github.com/SAP/ai-sdk-js/issues/1429 for tracking true cancellation support
30185
- *
30240
+ * aborted. However, this does not cancel the underlying HTTP request to SAP AI Core -
30241
+ * the request continues executing on the server. This is a current limitation of the
30242
+ * SAP AI SDK's API. See https://github.com/SAP/ai-sdk-js/issues/1429
30186
30243
  * @param options - Generation options including prompt, tools, and settings
30187
30244
  * @returns Promise resolving to the generation result with content, usage, and metadata
30188
- *
30245
+ * @since 1.0.0
30189
30246
  * @example
30190
30247
  * ```typescript
30191
30248
  * const result = await model.doGenerate({
@@ -30344,40 +30401,29 @@ var SAPAILanguageModel = class {
30344
30401
  /**
30345
30402
  * Generates a streaming completion.
30346
30403
  *
30347
- * This method implements the `LanguageModelV3.doStream` interface,
30348
- * sending a streaming request to SAP AI Core and returning a stream of response parts.
30404
+ * Implements `LanguageModelV3.doStream`, sending a streaming request to SAP AI Core
30405
+ * and returning a stream of response parts.
30349
30406
  *
30350
30407
  * **Stream Events:**
30351
- * - `stream-start` - Stream initialization with warnings
30352
- * - `response-metadata` - Response metadata (model, timestamp)
30408
+ * - `stream-start` - Initialization with warnings
30409
+ * - `response-metadata` - Model, timestamp, response ID
30353
30410
  * - `text-start` - Text block begins (with unique ID)
30354
- * - `text-delta` - Incremental text chunks (with block ID)
30355
- * - `text-end` - Text block completes (with accumulated text)
30356
- * - `tool-input-start` - Tool input begins
30357
- * - `tool-input-delta` - Tool input chunk
30358
- * - `tool-input-end` - Tool input completes
30411
+ * - `text-delta` - Incremental text chunks
30412
+ * - `text-end` - Text block completes
30413
+ * - `tool-input-start/delta/end` - Tool input lifecycle
30359
30414
  * - `tool-call` - Complete tool call
30360
30415
  * - `finish` - Stream completes with usage and finish reason
30361
30416
  * - `error` - Error occurred
30362
30417
  *
30363
- * **Stream Structure:**
30364
- * - Text blocks have explicit lifecycle with unique IDs
30365
- * - Finish reason format: `{ unified: string, raw?: string }`
30366
- * - Usage format: `{ inputTokens: { total, ... }, outputTokens: { total, ... } }`
30367
- * - Warnings only in `stream-start` event
30418
+ * **Response ID:**
30419
+ * Client-generated UUID in `response-metadata.id` and `providerMetadata['sap-ai'].responseId`.
30420
+ * TODO: Use backend's `x-request-id` when `OrchestrationStreamResponse` exposes `rawResponse`.
30368
30421
  *
30369
- * **Note on Abort Signal:**
30370
- * The abort signal implementation uses Promise.race to reject the promise when
30371
- * the signal is aborted. However, this does not cancel the underlying HTTP request
30372
- * to SAP AI Core - the request continues executing on the server. This is a
30373
- * limitation of the SAP AI SDK's chatCompletion API.
30374
- *
30375
- * @see https://github.com/SAP/ai-sdk-js/issues/1429 for tracking true cancellation support
30422
+ * **Abort Signal:**
30423
+ * Same limitation as `doGenerate` - see its documentation for details.
30376
30424
  * @see {@link https://sdk.vercel.ai/docs/ai-sdk-core/streaming Vercel AI SDK Streaming}
30377
- *
30378
30425
  * @param options - Streaming options including prompt, tools, and settings
30379
30426
  * @returns Promise resolving to stream and request metadata
30380
- *
30381
30427
  * @example
30382
30428
  * ```typescript
30383
30429
  * const { stream } = await model.doStream({
@@ -30390,13 +30436,9 @@ var SAPAILanguageModel = class {
30390
30436
  * if (part.type === 'text-delta') {
30391
30437
  * process.stdout.write(part.delta);
30392
30438
  * }
30393
- * if (part.type === 'text-end') {
30394
- * console.log('Block complete:', part.id, part.text);
30395
- * }
30396
30439
  * }
30397
30440
  * ```
30398
- *
30399
- * @since 4.0.0
30441
+ * @since 1.0.0
30400
30442
  */
30401
30443
  async doStream(options) {
30402
30444
  try {
@@ -30423,6 +30465,7 @@ var SAPAILanguageModel = class {
30423
30465
  promptTemplating: { include_usage: true }
30424
30466
  });
30425
30467
  const idGenerator = new StreamIdGenerator();
30468
+ const responseId = idGenerator.generateResponseId();
30426
30469
  let textBlockId = null;
30427
30470
  const streamState = {
30428
30471
  activeText: false,
@@ -30466,6 +30509,7 @@ var SAPAILanguageModel = class {
30466
30509
  if (streamState.isFirstChunk) {
30467
30510
  streamState.isFirstChunk = false;
30468
30511
  controller.enqueue({
30512
+ id: responseId,
30469
30513
  modelId,
30470
30514
  timestamp: /* @__PURE__ */ new Date(),
30471
30515
  type: "response-metadata"
@@ -30628,6 +30672,12 @@ var SAPAILanguageModel = class {
30628
30672
  }
30629
30673
  controller.enqueue({
30630
30674
  finishReason: streamState.finishReason,
30675
+ providerMetadata: {
30676
+ "sap-ai": {
30677
+ finishReason: streamState.finishReason.raw,
30678
+ responseId
30679
+ }
30680
+ },
30631
30681
  type: "finish",
30632
30682
  usage: streamState.usage
30633
30683
  });
@@ -30662,7 +30712,6 @@ var SAPAILanguageModel = class {
30662
30712
  }
30663
30713
  /**
30664
30714
  * Checks if a URL is supported for file/image uploads.
30665
- *
30666
30715
  * @param url - The URL to check
30667
30716
  * @returns True if the URL protocol is HTTPS or data with valid image format
30668
30717
  */
@@ -30675,7 +30724,6 @@ var SAPAILanguageModel = class {
30675
30724
  }
30676
30725
  /**
30677
30726
  * Builds orchestration module config for SAP AI SDK.
30678
- *
30679
30727
  * @param options - Call options from the AI SDK
30680
30728
  * @returns Object containing orchestration config, messages, and warnings
30681
30729
  * @internal
@@ -30850,7 +30898,6 @@ var SAPAILanguageModel = class {
30850
30898
  }
30851
30899
  /**
30852
30900
  * Creates an OrchestrationClient instance.
30853
- *
30854
30901
  * @param config - Orchestration module configuration
30855
30902
  * @returns OrchestrationClient instance
30856
30903
  * @internal