@jerome-benoit/sap-ai-provider 3.0.0 → 4.0.0-rc.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
@@ -24771,7 +24771,7 @@ var require_form_data = __commonJS({
24771
24771
  var parseUrl = __require("url").parse;
24772
24772
  var fs = __require("fs");
24773
24773
  var Stream = __require("stream").Stream;
24774
- var crypto = __require("crypto");
24774
+ var crypto2 = __require("crypto");
24775
24775
  var mime = require_mime_types();
24776
24776
  var asynckit = require_asynckit();
24777
24777
  var setToStringTag = require_es_set_tostringtag();
@@ -24977,7 +24977,7 @@ var require_form_data = __commonJS({
24977
24977
  return Buffer.concat([dataBuffer, Buffer.from(this._lastBoundary())]);
24978
24978
  };
24979
24979
  FormData2.prototype._generateBoundary = function() {
24980
- this._boundary = "--------------------------" + crypto.randomBytes(12).toString("hex");
24980
+ this._boundary = "--------------------------" + crypto2.randomBytes(12).toString("hex");
24981
24981
  };
24982
24982
  FormData2.prototype.getLengthSync = function() {
24983
24983
  var knownLength = this._overheadLength + this._valueLength;
@@ -26207,7 +26207,7 @@ var require_axios = __commonJS({
26207
26207
  "node_modules/axios/dist/node/axios.cjs"(exports, module) {
26208
26208
  "use strict";
26209
26209
  var FormData$1 = require_form_data();
26210
- var crypto = __require("crypto");
26210
+ var crypto2 = __require("crypto");
26211
26211
  var url = __require("url");
26212
26212
  var proxyFromEnv = require_proxy_from_env();
26213
26213
  var http = __require("http");
@@ -26222,7 +26222,7 @@ var require_axios = __commonJS({
26222
26222
  return e && typeof e === "object" && "default" in e ? e : { "default": e };
26223
26223
  }
26224
26224
  var FormData__default = /* @__PURE__ */ _interopDefaultLegacy(FormData$1);
26225
- var crypto__default = /* @__PURE__ */ _interopDefaultLegacy(crypto);
26225
+ var crypto__default = /* @__PURE__ */ _interopDefaultLegacy(crypto2);
26226
26226
  var url__default = /* @__PURE__ */ _interopDefaultLegacy(url);
26227
26227
  var proxyFromEnv__default = /* @__PURE__ */ _interopDefaultLegacy(proxyFromEnv);
26228
26228
  var http__default = /* @__PURE__ */ _interopDefaultLegacy(http);
@@ -29888,12 +29888,14 @@ function convertToSAPMessages(prompt, options = {}) {
29888
29888
  }
29889
29889
  case "tool": {
29890
29890
  for (const part of message.content) {
29891
- const toolMessage = {
29892
- role: "tool",
29893
- tool_call_id: part.toolCallId,
29894
- content: JSON.stringify(part.output)
29895
- };
29896
- messages.push(toolMessage);
29891
+ if (part.type === "tool-result") {
29892
+ const toolMessage = {
29893
+ role: "tool",
29894
+ tool_call_id: part.toolCallId,
29895
+ content: JSON.stringify(part.output)
29896
+ };
29897
+ messages.push(toolMessage);
29898
+ }
29897
29899
  }
29898
29900
  break;
29899
29901
  }
@@ -30062,6 +30064,16 @@ See: https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-serv
30062
30064
  }
30063
30065
 
30064
30066
  // src/sap-ai-chat-language-model.ts
30067
+ var StreamIdGenerator = class {
30068
+ /**
30069
+ * Generates a unique ID for a text block.
30070
+ *
30071
+ * @returns RFC 4122-compliant UUID string
30072
+ */
30073
+ generateTextBlockId() {
30074
+ return crypto.randomUUID();
30075
+ }
30076
+ };
30065
30077
  function validateModelParameters(params, warnings) {
30066
30078
  if (params.temperature !== void 0 && (params.temperature < 0 || params.temperature > 2)) {
30067
30079
  warnings.push({
@@ -30153,7 +30165,7 @@ function buildSAPToolParameters(schema) {
30153
30165
  };
30154
30166
  }
30155
30167
  var SAPAIChatLanguageModel = class {
30156
- specificationVersion = "v2";
30168
+ specificationVersion = "v3";
30157
30169
  modelId;
30158
30170
  config;
30159
30171
  settings;
@@ -30176,10 +30188,14 @@ var SAPAIChatLanguageModel = class {
30176
30188
  * Checks if a URL is supported for file/image uploads.
30177
30189
  *
30178
30190
  * @param url - The URL to check
30179
- * @returns True if the URL protocol is HTTPS or data (content-type rules are enforced via supportedUrls)
30191
+ * @returns True if the URL protocol is HTTPS or data with valid image format
30180
30192
  */
30181
30193
  supportsUrl(url) {
30182
- return url.protocol === "https:" || url.protocol === "data:";
30194
+ if (url.protocol === "https:") return true;
30195
+ if (url.protocol === "data:") {
30196
+ return /^data:image\//i.test(url.href);
30197
+ }
30198
+ return false;
30183
30199
  }
30184
30200
  /**
30185
30201
  * Returns supported URL patterns for different content types.
@@ -30192,9 +30208,45 @@ var SAPAIChatLanguageModel = class {
30192
30208
  };
30193
30209
  }
30194
30210
  /**
30195
- * Gets the provider identifier.
30211
+ * Generates text completion using SAP AI Core's Orchestration API.
30212
+ *
30213
+ * This method implements the `LanguageModelV3.doGenerate` interface,
30214
+ * providing synchronous (non-streaming) text generation with support for:
30215
+ * - Multi-turn conversations with system/user/assistant messages
30216
+ * - Tool calling (function calling) with structured outputs
30217
+ * - Multi-modal inputs (text + images)
30218
+ * - Data masking via SAP DPI
30219
+ * - Content filtering via Azure Content Safety or Llama Guard
30220
+ *
30221
+ * **Return Structure:**
30222
+ * - Finish reason: `{ unified: string, raw?: string }`
30223
+ * - Usage: Nested structure with token breakdown `{ inputTokens: { total, ... }, outputTokens: { total, ... } }`
30224
+ * - Warnings: Array of warnings with `type` and optional `feature` field
30225
+ *
30226
+ * @param options - Generation options including prompt, tools, temperature, etc.
30227
+ * @returns Promise resolving to generation result with content, usage, and metadata
30228
+ *
30229
+ * @throws {InvalidPromptError} If prompt format is invalid
30230
+ * @throws {InvalidArgumentError} If arguments are malformed
30231
+ * @throws {APICallError} If the SAP AI Core API call fails
30232
+ *
30233
+ * @example
30234
+ * ```typescript
30235
+ * const result = await model.doGenerate({
30236
+ * prompt: [
30237
+ * { role: 'user', content: [{ type: 'text', text: 'Hello!' }] }
30238
+ * ],
30239
+ * temperature: 0.7,
30240
+ * maxTokens: 100
30241
+ * });
30242
+ *
30243
+ * console.log(result.content); // Array of V3 content parts
30244
+ * console.log(result.finishReason.unified); // "stop", "length", "tool-calls", etc.
30245
+ * console.log(result.usage.inputTokens.total); // Total input tokens
30246
+ * ```
30196
30247
  *
30197
- * @returns The provider name ('sap-ai')
30248
+ * @since 1.0.0
30249
+ * @since 4.0.0 Updated to LanguageModelV3 interface
30198
30250
  */
30199
30251
  get provider() {
30200
30252
  return this.config.provider;
@@ -30261,11 +30313,11 @@ var SAPAIChatLanguageModel = class {
30261
30313
  const schemaRecord = jsonSchema;
30262
30314
  delete schemaRecord.$schema;
30263
30315
  parameters = buildSAPToolParameters(schemaRecord);
30264
- } catch {
30316
+ } catch (error) {
30265
30317
  warnings.push({
30266
- type: "unsupported-tool",
30267
- tool,
30268
- details: "Failed to convert tool Zod schema to JSON Schema. Falling back to empty object schema."
30318
+ type: "unsupported",
30319
+ feature: `tool schema conversion for ${tool.name}`,
30320
+ details: `Failed to convert tool Zod schema: ${error instanceof Error ? error.message : String(error)}. Falling back to empty object schema.`
30269
30321
  });
30270
30322
  parameters = buildSAPToolParameters({});
30271
30323
  }
@@ -30289,8 +30341,9 @@ var SAPAIChatLanguageModel = class {
30289
30341
  };
30290
30342
  } else {
30291
30343
  warnings.push({
30292
- type: "unsupported-tool",
30293
- tool
30344
+ type: "unsupported",
30345
+ feature: `tool type for ${tool.name}`,
30346
+ details: "Only 'function' tool type is supported."
30294
30347
  });
30295
30348
  return null;
30296
30349
  }
@@ -30342,8 +30395,8 @@ var SAPAIChatLanguageModel = class {
30342
30395
  );
30343
30396
  if (options.toolChoice && options.toolChoice.type !== "auto") {
30344
30397
  warnings.push({
30345
- type: "unsupported-setting",
30346
- setting: "toolChoice",
30398
+ type: "unsupported",
30399
+ feature: "toolChoice",
30347
30400
  details: `SAP AI SDK does not support toolChoice '${options.toolChoice.type}'. Using default 'auto' behavior.`
30348
30401
  });
30349
30402
  }
@@ -30403,7 +30456,7 @@ var SAPAIChatLanguageModel = class {
30403
30456
  /**
30404
30457
  * Generates a single completion (non-streaming).
30405
30458
  *
30406
- * This method implements the `LanguageModelV2.doGenerate` interface,
30459
+ * This method implements the `LanguageModelV3.doGenerate` interface,
30407
30460
  * sending a request to SAP AI Core and returning the complete response.
30408
30461
  *
30409
30462
  * **Features:**
@@ -30411,6 +30464,13 @@ var SAPAIChatLanguageModel = class {
30411
30464
  * - Multi-modal input (text + images)
30412
30465
  * - Data masking (if configured)
30413
30466
  * - Content filtering (if configured)
30467
+ * - Abort signal support (via Promise.race)
30468
+ *
30469
+ * **Note on Abort Signal:**
30470
+ * The abort signal implementation uses Promise.race to reject the promise when
30471
+ * the signal is aborted. However, this does not cancel the underlying HTTP request
30472
+ * to SAP AI Core - the request continues executing on the server. This is a
30473
+ * limitation of the SAP AI SDK's chatCompletion API.
30414
30474
  *
30415
30475
  * @param options - Generation options including prompt, tools, and settings
30416
30476
  * @returns Promise resolving to the generation result with content, usage, and metadata
@@ -30483,7 +30543,7 @@ var SAPAIChatLanguageModel = class {
30483
30543
  Object.entries(responseHeadersRaw).flatMap(([key, value]) => {
30484
30544
  if (typeof value === "string") return [[key, value]];
30485
30545
  if (Array.isArray(value)) {
30486
- const strings = value.filter((item) => typeof item === "string").join(",");
30546
+ const strings = value.filter((item) => typeof item === "string").join("; ");
30487
30547
  return strings.length > 0 ? [[key, strings]] : [];
30488
30548
  }
30489
30549
  if (typeof value === "number" || typeof value === "boolean") {
@@ -30524,9 +30584,17 @@ var SAPAIChatLanguageModel = class {
30524
30584
  content,
30525
30585
  finishReason,
30526
30586
  usage: {
30527
- inputTokens: tokenUsage.prompt_tokens,
30528
- outputTokens: tokenUsage.completion_tokens,
30529
- totalTokens: tokenUsage.total_tokens
30587
+ inputTokens: {
30588
+ total: tokenUsage.prompt_tokens,
30589
+ noCache: tokenUsage.prompt_tokens,
30590
+ cacheRead: void 0,
30591
+ cacheWrite: void 0
30592
+ },
30593
+ outputTokens: {
30594
+ total: tokenUsage.completion_tokens,
30595
+ text: tokenUsage.completion_tokens,
30596
+ reasoning: void 0
30597
+ }
30530
30598
  },
30531
30599
  providerMetadata: {
30532
30600
  "sap-ai": {
@@ -30557,19 +30625,28 @@ var SAPAIChatLanguageModel = class {
30557
30625
  /**
30558
30626
  * Generates a streaming completion.
30559
30627
  *
30560
- * This method implements the `LanguageModelV2.doStream` interface,
30628
+ * This method implements the `LanguageModelV3.doStream` interface,
30561
30629
  * sending a streaming request to SAP AI Core and returning a stream of response parts.
30562
30630
  *
30563
30631
  * **Stream Events:**
30564
- * - `stream-start` - Stream initialization
30632
+ * - `stream-start` - Stream initialization with warnings
30565
30633
  * - `response-metadata` - Response metadata (model, timestamp)
30566
- * - `text-start` - Text generation starts
30567
- * - `text-delta` - Incremental text chunks
30568
- * - `text-end` - Text generation completes
30569
- * - `tool-call` - Tool call detected
30634
+ * - `text-start` - Text block begins (with unique ID)
30635
+ * - `text-delta` - Incremental text chunks (with block ID)
30636
+ * - `text-end` - Text block completes (with accumulated text)
30637
+ * - `tool-input-start` - Tool input begins
30638
+ * - `tool-input-delta` - Tool input chunk
30639
+ * - `tool-input-end` - Tool input completes
30640
+ * - `tool-call` - Complete tool call
30570
30641
  * - `finish` - Stream completes with usage and finish reason
30571
30642
  * - `error` - Error occurred
30572
30643
  *
30644
+ * **Stream Structure:**
30645
+ * - Text blocks have explicit lifecycle with unique IDs
30646
+ * - Finish reason format: `{ unified: string, raw?: string }`
30647
+ * - Usage format: `{ inputTokens: { total, ... }, outputTokens: { total, ... } }`
30648
+ * - Warnings only in `stream-start` event
30649
+ *
30573
30650
  * @param options - Streaming options including prompt, tools, and settings
30574
30651
  * @returns Promise resolving to stream and request metadata
30575
30652
  *
@@ -30585,8 +30662,13 @@ var SAPAIChatLanguageModel = class {
30585
30662
  * if (part.type === 'text-delta') {
30586
30663
  * process.stdout.write(part.delta);
30587
30664
  * }
30665
+ * if (part.type === 'text-end') {
30666
+ * console.log('Block complete:', part.id, part.text);
30667
+ * }
30588
30668
  * }
30589
30669
  * ```
30670
+ *
30671
+ * @since 4.0.0
30590
30672
  */
30591
30673
  async doStream(options) {
30592
30674
  try {
@@ -30614,12 +30696,25 @@ var SAPAIChatLanguageModel = class {
30614
30696
  options.abortSignal,
30615
30697
  { promptTemplating: { include_usage: true } }
30616
30698
  );
30699
+ const idGenerator = new StreamIdGenerator();
30700
+ let textBlockId = null;
30617
30701
  const streamState = {
30618
- finishReason: "unknown",
30702
+ finishReason: {
30703
+ unified: "other",
30704
+ raw: void 0
30705
+ },
30619
30706
  usage: {
30620
- inputTokens: void 0,
30621
- outputTokens: void 0,
30622
- totalTokens: void 0
30707
+ inputTokens: {
30708
+ total: void 0,
30709
+ noCache: void 0,
30710
+ cacheRead: void 0,
30711
+ cacheWrite: void 0
30712
+ },
30713
+ outputTokens: {
30714
+ total: void 0,
30715
+ text: void 0,
30716
+ reasoning: void 0
30717
+ }
30623
30718
  },
30624
30719
  isFirstChunk: true,
30625
30720
  activeText: false
@@ -30647,19 +30742,25 @@ var SAPAIChatLanguageModel = class {
30647
30742
  }
30648
30743
  const deltaToolCalls = chunk.getDeltaToolCalls();
30649
30744
  if (Array.isArray(deltaToolCalls) && deltaToolCalls.length > 0) {
30650
- streamState.finishReason = "tool-calls";
30745
+ streamState.finishReason = {
30746
+ unified: "tool-calls",
30747
+ raw: void 0
30748
+ };
30651
30749
  }
30652
30750
  const deltaContent = chunk.getDeltaContent();
30653
- if (typeof deltaContent === "string" && deltaContent.length > 0 && streamState.finishReason !== "tool-calls") {
30751
+ if (typeof deltaContent === "string" && deltaContent.length > 0 && streamState.finishReason.unified !== "tool-calls") {
30654
30752
  if (!streamState.activeText) {
30655
- controller.enqueue({ type: "text-start", id: "0" });
30753
+ textBlockId = idGenerator.generateTextBlockId();
30754
+ controller.enqueue({ type: "text-start", id: textBlockId });
30656
30755
  streamState.activeText = true;
30657
30756
  }
30658
- controller.enqueue({
30659
- type: "text-delta",
30660
- id: "0",
30661
- delta: deltaContent
30662
- });
30757
+ if (textBlockId) {
30758
+ controller.enqueue({
30759
+ type: "text-delta",
30760
+ id: textBlockId,
30761
+ delta: deltaContent
30762
+ });
30763
+ }
30663
30764
  }
30664
30765
  if (Array.isArray(deltaToolCalls) && deltaToolCalls.length > 0) {
30665
30766
  for (const toolCallChunk of deltaToolCalls) {
@@ -30709,7 +30810,7 @@ var SAPAIChatLanguageModel = class {
30709
30810
  const chunkFinishReason = chunk.getFinishReason();
30710
30811
  if (chunkFinishReason) {
30711
30812
  streamState.finishReason = mapFinishReason(chunkFinishReason);
30712
- if (streamState.finishReason === "tool-calls") {
30813
+ if (streamState.finishReason.unified === "tool-calls") {
30713
30814
  const toolCalls2 = Array.from(toolCallsInProgress.values());
30714
30815
  for (const tc of toolCalls2) {
30715
30816
  if (tc.didEmitCall) {
@@ -30738,8 +30839,8 @@ var SAPAIChatLanguageModel = class {
30738
30839
  input: tc.arguments
30739
30840
  });
30740
30841
  }
30741
- if (streamState.activeText) {
30742
- controller.enqueue({ type: "text-end", id: "0" });
30842
+ if (streamState.activeText && textBlockId) {
30843
+ controller.enqueue({ type: "text-end", id: textBlockId });
30743
30844
  streamState.activeText = false;
30744
30845
  }
30745
30846
  }
@@ -30775,20 +30876,24 @@ var SAPAIChatLanguageModel = class {
30775
30876
  input: tc.arguments
30776
30877
  });
30777
30878
  }
30778
- if (streamState.activeText) {
30779
- controller.enqueue({ type: "text-end", id: "0" });
30879
+ if (streamState.activeText && textBlockId) {
30880
+ controller.enqueue({ type: "text-end", id: textBlockId });
30780
30881
  }
30781
30882
  const finalFinishReason = streamResponse.getFinishReason();
30782
30883
  if (finalFinishReason) {
30783
30884
  streamState.finishReason = mapFinishReason(finalFinishReason);
30784
30885
  } else if (didEmitAnyToolCalls) {
30785
- streamState.finishReason = "tool-calls";
30886
+ streamState.finishReason = {
30887
+ unified: "tool-calls",
30888
+ raw: void 0
30889
+ };
30786
30890
  }
30787
30891
  const finalUsage = streamResponse.getTokenUsage();
30788
30892
  if (finalUsage) {
30789
- streamState.usage.inputTokens = finalUsage.prompt_tokens;
30790
- streamState.usage.outputTokens = finalUsage.completion_tokens;
30791
- streamState.usage.totalTokens = finalUsage.total_tokens;
30893
+ streamState.usage.inputTokens.total = finalUsage.prompt_tokens;
30894
+ streamState.usage.inputTokens.noCache = finalUsage.prompt_tokens;
30895
+ streamState.usage.outputTokens.total = finalUsage.completion_tokens;
30896
+ streamState.usage.outputTokens.text = finalUsage.completion_tokens;
30792
30897
  }
30793
30898
  controller.enqueue({
30794
30899
  type: "finish",
@@ -30819,8 +30924,7 @@ var SAPAIChatLanguageModel = class {
30819
30924
  stream: transformedStream,
30820
30925
  request: {
30821
30926
  body: requestBody
30822
- },
30823
- warnings: warningsOut
30927
+ }
30824
30928
  };
30825
30929
  } catch (error) {
30826
30930
  throw convertToAISDKError(error, {
@@ -30832,27 +30936,28 @@ var SAPAIChatLanguageModel = class {
30832
30936
  }
30833
30937
  };
30834
30938
  function mapFinishReason(reason) {
30835
- if (!reason) return "unknown";
30939
+ const raw = reason;
30940
+ if (!reason) return { unified: "other", raw };
30836
30941
  switch (reason.toLowerCase()) {
30837
30942
  case "stop":
30838
30943
  case "end_turn":
30839
30944
  case "stop_sequence":
30840
30945
  case "eos":
30841
- return "stop";
30946
+ return { unified: "stop", raw };
30842
30947
  case "length":
30843
30948
  case "max_tokens":
30844
30949
  case "max_tokens_reached":
30845
- return "length";
30950
+ return { unified: "length", raw };
30846
30951
  case "tool_calls":
30847
30952
  case "tool_call":
30848
30953
  case "function_call":
30849
- return "tool-calls";
30954
+ return { unified: "tool-calls", raw };
30850
30955
  case "content_filter":
30851
- return "content-filter";
30956
+ return { unified: "content-filter", raw };
30852
30957
  case "error":
30853
- return "error";
30958
+ return { unified: "error", raw };
30854
30959
  default:
30855
- return "other";
30960
+ return { unified: "other", raw };
30856
30961
  }
30857
30962
  }
30858
30963