diff --git a/.changeset/better-comics-pay.md b/.changeset/better-comics-pay.md new file mode 100644 index 00000000000..e3a18e2e976 --- /dev/null +++ b/.changeset/better-comics-pay.md @@ -0,0 +1,7 @@ +--- +"kilo-code": minor +"kilocode-docs": patch +"@roo-code/types": patch +--- + +Added SAP AI Core provider diff --git a/apps/kilocode-docs/docs/providers/sap-ai-core.md b/apps/kilocode-docs/docs/providers/sap-ai-core.md new file mode 100644 index 00000000000..ac407a03666 --- /dev/null +++ b/apps/kilocode-docs/docs/providers/sap-ai-core.md @@ -0,0 +1,150 @@ +--- +sidebar_label: SAP AI Core +--- + +# Using SAP AI Core With Kilo Code + +Kilo Code supports accessing models through SAP AI Core, a service in the SAP Business Technology Platform that lets you efficiently run AI scenarios in a standardized, scalable, and hyperscaler-agnostic manner. + +**Website:** [https://help.sap.com/docs/sap-ai-core](https://help.sap.com/docs/sap-ai-core) + +## Prerequisites + +- **SAP BTP Account:** You need an active SAP Business Technology Platform account. +- **SAP AI Core Service:** You must have access to the SAP AI Core service in your BTP subaccount. +- **Service Instance:** Create a service instance of SAP AI Core with appropriate service plan. +- **Service Key:** Generate a service key for your SAP AI Core service instance to obtain the required credentials. + +## Getting Credentials + +To use SAP AI Core with Kilo Code, you'll need to create a service key for your SAP AI Core service instance: + +1. **In SAP BTP Cockpit:** + + - Navigate to your subaccount + - Go to "Services" → "Instances and Subscriptions" + - Find your SAP AI Core service instance + - Create a new service key + +2. **Service Key Information:** + The service key will contain the following information you'll need: + - **Client ID:** OAuth2 client identifier + - **Client Secret:** OAuth2 client secret + - **Auth URL:** OAuth2 authentication endpoint + - **Base URL:** SAP AI Core API base URL + - **Resource Group:** (Optional) Specify a resource group, defaults to "default" + +## Operating Modes + +SAP AI Core provider supports two operating modes: + +### Foundation Models Mode (Default) + +- Uses foundation models that require active deployments +- Currently, supports **OpenAI models only** due to SAP AI Core SDK limitations +- Requires you to have running deployments for the models you want to use +- Models must have deployments in "RUNNING" status to be selectable + +### Orchestration Mode + +- Uses SAP AI Core's orchestration capabilities +- Supports models from multiple providers: **Amazon, Anthropic, Google, OpenAI, and Mistral AI** +- Does not require separate deployments +- Provides access to a broader range of models + +## Model Requirements + +Kilo Code applies the following filters when fetching models: + +- **Streaming:** Models must support streaming +- **Capabilities:** Models must support text generation +- **Context Window:** Models must have a context window of at least 32,000 tokens + +## Supported Providers + +### Foundation Models Mode + +- **OpenAI:** All OpenAI models with active deployments + +### Orchestration Mode + +- **Amazon:** Amazon foundation models +- **Anthropic:** Claude models +- **Google:** Gemini models +- **OpenAI:** ChatGPT and GPT models +- **Mistral AI:** Mistral AI models + +The exact list of available models depends on your SAP AI Core configuration and active model offerings. + +## Configuration in Kilo Code + +1. **Open Kilo Code Settings:** Click the gear icon () in the Kilo Code panel. +2. **Select Provider:** Choose "SAP AI Core" from the "API Provider" dropdown. +3. **Enter Credentials:** + - **Client ID:** Enter your SAP AI Core OAuth2 client ID + - **Client Secret:** Enter your SAP AI Core OAuth2 client secret + - **Base URL:** Enter your SAP AI Core API base URL (e.g., `https://api.ai.ml.hana.ondemand.com`) + - **Auth URL:** Enter your SAP AI Core OAuth2 auth URL (e.g., `https://your-subdomain.authentication.sap.hana.ondemand.com`) + - **Resource Group:** (Optional) Enter your resource group name, defaults to "default" +4. **Choose Operating Mode:** + - **Orchestration Mode:** Check the "Use Orchestration" checkbox for broader model access + - **Foundation Models Mode:** Leave unchecked to use foundation models with deployments +5. **Select Model:** Choose your desired model from the dropdown +6. **Select Deployment:** (Foundation Models Mode only) Choose an active deployment for your selected model + +## Deployments (Foundation Models Mode) + +When using Foundation Models mode: + +- You must have active deployments for the models you want to use +- Only deployments with "RUNNING" status are available for selection +- Deployments in other states (PENDING, STOPPED, etc.) are shown but disabled +- The interface displays the number of available deployments for each model + +## Tips and Notes + +- **Authentication:** SAP AI Core uses OAuth2 client credentials flow for authentication +- **Caching:** Model and deployment information is cached for 15 and 5 minutes respectively to improve performance +- **Resource Groups:** If you use multiple resource groups, specify the appropriate one in the configuration +- **Permissions:** Ensure your service key has the necessary permissions to access models and deployments +- **Orchestration Benefits:** Use Orchestration mode for access to a wider variety of models without managing deployments +- **Foundation Models Benefits:** Use Foundation Models mode when you need more control over specific model deployments + +## Troubleshooting + +### Common Issues + +1. **Authentication Failures:** + + - Verify your Client ID and Client Secret are correct + - Check that your Auth URL is properly formatted + - Ensure your service key hasn't expired + +2. **No Models Available:** + + - Check that you have the necessary permissions in your resource group + - Verify your Base URL is correct + - In Foundation Models mode, ensure you have running deployments + +3. **Deployment Issues:** + + - Check that your deployments are in "RUNNING" status + - Verify you're using the correct resource group + - Review your SAP AI Core service configuration + +4. **Model Access:** + - In Foundation Models mode, **only OpenAI models** are currently supported + - Switch to Orchestration mode for access to other providers + - Ensure models meet the minimum requirements (32k context window, streaming support) + +## Getting Started + +To get started with SAP AI Core: + +1. Set up your SAP BTP account and access SAP AI Core service +2. Create a service instance and generate a service key +3. Configure Kilo Code with your credentials +4. Choose between Foundation Models or Orchestration mode based on your needs +5. Select an appropriate model and start coding + +For detailed setup instructions and service configuration, visit the [SAP AI Core documentation](https://help.sap.com/docs/sap-ai-core). diff --git a/cli/src/services/logs.ts b/cli/src/services/logs.ts index 8ac6190fb14..2d7fc14dad5 100644 --- a/cli/src/services/logs.ts +++ b/cli/src/services/logs.ts @@ -78,7 +78,7 @@ export class LogsService { stack: error.stack, // Include any additional enumerable properties ...Object.getOwnPropertyNames(error) - .filter(key => key !== "message" && key !== "name" && key !== "stack") + .filter((key) => key !== "message" && key !== "name" && key !== "stack") .reduce( (acc, key) => { acc[key] = (error as any)[key] diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index dad1c27e635..289ca770024 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -235,6 +235,7 @@ export const SECRET_STATE_KEYS = [ "ioIntelligenceApiKey", "vercelAiGatewayApiKey", "ovhCloudAiEndpointsApiKey", // kilocode_change + "sapAiCoreServiceKey", ] as const // Global secrets that are part of GlobalSettings (not ProviderSettings) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 5ef923a0f65..2dfd031fa05 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -59,6 +59,7 @@ export const dynamicProviders = [ "requesty", "unbound", "glama", + "sap-ai-core", ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -150,6 +151,7 @@ export const providerNames = [ "virtual-quota-fallback", "synthetic", // kilocode_change end + "sap-ai-core", "sambanova", "vertex", "xai", @@ -490,6 +492,15 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({ vercelAiGatewayModelId: z.string().optional(), }) +const sapAiCoreSchema = baseProviderSettingsSchema.extend({ + sapAiCoreServiceKey: z.string().optional(), + sapAiCoreResourceGroup: z.string().optional(), + sapAiCoreUseOrchestration: z.boolean().optional(), + sapAiCoreModelId: z.string().optional(), + sapAiCoreDeploymentId: z.string().optional(), + sapAiCoreCustomModelInfo: modelInfoSchema.nullish(), +}) + const defaultSchema = z.object({ apiProvider: z.undefined(), }) @@ -537,6 +548,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), + sapAiCoreSchema.merge(z.object({ apiProvider: z.literal("sap-ai-core") })), defaultSchema, ]) @@ -583,6 +595,7 @@ export const providerSettingsSchema = z.object({ ...qwenCodeSchema.shape, ...rooSchema.shape, ...vercelAiGatewaySchema.shape, + ...sapAiCoreSchema.shape, ...codebaseIndexProviderSchema.shape, ...ovhcloudSchema.shape, // kilocode_change }) @@ -620,6 +633,7 @@ export const modelIdKeys = [ "deepInfraModelId", "kilocodeModel", "ovhCloudAiEndpointsModelId", // kilocode_change + "sapAiCoreModelId", ] as const satisfies readonly (keyof ProviderSettings)[] export type ModelIdKey = (typeof modelIdKeys)[number] @@ -676,6 +690,7 @@ export const modelIdKeysByProvider: Record = { kilocode: "kilocodeModel", "virtual-quota-fallback": "apiModelId", ovhcloud: "ovhCloudAiEndpointsModelId", // kilocode_change + "sap-ai-core": "sapAiCoreModelId", } /** @@ -808,6 +823,7 @@ export const MODELS_BY_PROVIDER: Record< requesty: { id: "requesty", label: "Requesty", models: [] }, unbound: { id: "unbound", label: "Unbound", models: [] }, ovhcloud: { id: "ovhcloud", label: "OVHcloud AI Endpoints", models: [] }, // kilocode_change + "sap-ai-core": { id: "sap-ai-core", label: "SAP AI Core", models: [] }, // kilocode_change start kilocode: { id: "kilocode", label: "Kilocode", models: [] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d82111683bb..fcb648d2bab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,22 +228,22 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: ^4.0.1 - version: 4.1.1(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + version: 4.1.1(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) '@storybook/addon-docs': specifier: ^9.0.18 - version: 9.1.3(@types/react@18.3.23)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + version: 9.1.3(@types/react@18.3.23)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) '@storybook/addon-links': specifier: ^9.0.18 - version: 9.1.3(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + version: 9.1.3(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) '@storybook/react-vite': specifier: ^9.0.18 - version: 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@storybook/test-runner': specifier: ^0.23.0 - version: 0.23.0(@types/node@24.2.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + version: 0.23.0(@types/node@24.2.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.6(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.1.6(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@types/react': specifier: ^18.3.23 version: 18.3.23 @@ -258,7 +258,7 @@ importers: version: 5.1.34 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.4.1(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.4.1(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) chromatic: specifier: ^13.0.0 version: 13.1.3 @@ -273,7 +273,7 @@ importers: version: 6.0.1 storybook: specifier: ^9.0.18 - version: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@4.1.8) @@ -282,7 +282,7 @@ importers: version: 5.8.3 vite: specifier: 6.3.5 - version: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) apps/vscode-e2e: devDependencies: @@ -457,7 +457,7 @@ importers: version: 4.1.6 vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) apps/web-roo-code: dependencies: @@ -1363,7 +1363,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.0 - version: 8.5.0(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.1) util: specifier: ^0.12.4 version: 0.12.5 @@ -1412,7 +1412,7 @@ importers: version: 20.17.57 vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) packages/cloud: dependencies: @@ -1452,7 +1452,7 @@ importers: version: 16.3.0 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) packages/config-eslint: devDependencies: @@ -1554,7 +1554,7 @@ importers: version: 4.19.4 vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) packages/ipc: dependencies: @@ -1579,7 +1579,7 @@ importers: version: 9.2.3 vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) packages/telemetry: dependencies: @@ -1607,7 +1607,7 @@ importers: version: 1.100.0 vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) packages/types: dependencies: @@ -1629,10 +1629,10 @@ importers: version: 16.3.0 tsup: specifier: ^8.3.5 - version: 8.5.0(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.1) vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) src: dependencies: @@ -1681,6 +1681,12 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../packages/types + '@sap-ai-sdk/foundation-models': + specifier: ^2.0.0 + version: 2.1.0 + '@sap-ai-sdk/orchestration': + specifier: ^2.0.0 + version: 2.1.0 '@vscode/codicons': specifier: ^0.0.36 version: 0.0.36 @@ -2065,7 +2071,7 @@ importers: version: link:../packages/types '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.6(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.1.6(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@tanstack/react-query': specifier: ^5.68.0 version: 5.76.1(react@18.3.1) @@ -2243,7 +2249,7 @@ importers: version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/react-vite': specifier: ^8.4.7 - version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@storybook/testing-library': specifier: ^0.2.2 version: 0.2.2 @@ -2282,7 +2288,7 @@ importers: version: 1.57.5 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@vitest/ui': specifier: ^3.2.3 version: 3.2.4(vitest@3.2.4) @@ -2312,10 +2318,10 @@ importers: version: 5.8.3 vite: specifier: 6.3.6 - version: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) vitest: specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) packages: @@ -3457,6 +3463,10 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@corex/deepmerge@4.0.43': resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} @@ -3746,6 +3756,9 @@ packages: peerDependencies: postcss: ^8.4 + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -6172,6 +6185,44 @@ packages: cpu: [x64] os: [win32] + '@sap-ai-sdk/ai-api@2.1.0': + resolution: {integrity: sha512-zmG+TufFLTSx9G+s5lkWhYCoslQZdNhCM5AbmvsyVm4U6gD6i9Q+ShCGJRhW9QOxJkNATFIUI+qNplAkHVnSlA==} + + '@sap-ai-sdk/core@2.1.0': + resolution: {integrity: sha512-RVds/ArJR3jMTP1iuMS8TPHPHqvyO3vGYpdxIVNaDydtKdBwnR4crcbJbAkbAzP5HuakjIK1R0JLxro7XN2Cwg==} + + '@sap-ai-sdk/foundation-models@2.1.0': + resolution: {integrity: sha512-FRwjNBXgTzTWWqmWGbqJkTt5wwqnsx5t96dPON1cNzD/nCQPqiT7Ke771+tGE2ZH9JcigQrfs/9p7mDL2czoXg==} + + '@sap-ai-sdk/orchestration@2.1.0': + resolution: {integrity: sha512-1WahSmmoD2aUW1ipFw+ioKIPCuA01QGoF1Rhx6nyVCeRq9y8WXciO70JJzBa0LAy5zF/zrNOeODeHF4gh1rXNg==} + + '@sap-ai-sdk/prompt-registry@2.1.0': + resolution: {integrity: sha512-vCfJv8Er8knfcwKRYBiEaRWXgb6iYz8f/t4ZU86ad7EyjKfEVb/KRQVw2rGs1p9XW5QCJpdE5/S2h6zSvhErdA==} + + '@sap-cloud-sdk/connectivity@4.1.2': + resolution: {integrity: sha512-PJNBg+yhyo4Y/PZBcqbN8eD+RFVolgOcWgqVTdHef6Wch8t6SVlDqxLzf4mJ4nTpAY2E3ERFjIwlP+MUUXcf5A==} + + '@sap-cloud-sdk/http-client@4.1.2': + resolution: {integrity: sha512-qQny7j22oyZxMZ64S6FSmtt5CCB4JyZND043ZTURamoIyuXsM3RegRN+rhjzwoBP4NLws4Rt4VfUOQ/X+DXXFQ==} + + '@sap-cloud-sdk/openapi@4.1.2': + resolution: {integrity: sha512-KJ/xjnmvKwLhlUv6duoH6pLDYS34NHryS44j5vSTJMK4EzIfKo2R62RKAM3Pm+3zkWnQh9U2Q48Hbpornt8k1Q==} + + '@sap-cloud-sdk/resilience@4.1.2': + resolution: {integrity: sha512-rMcM6Sn0WswNQK9583UCBEwzqvPSETae8GzzFrTQ7+JM6RA6PU5/WiPmhqzQQw1MFQvwuX6uMDwPf4U3AdemMQ==} + + '@sap-cloud-sdk/util@4.1.2': + resolution: {integrity: sha512-lsxsBc60pokMDCHvs0/CXTa6fjVEMzTeSyB8CerG6L0sN4sK8Ppm63VVcSh/E+X+U4aVMHmLuOmmd6XyLGAc3Q==} + + '@sap/xsenv@6.0.0': + resolution: {integrity: sha512-9bNpJXmxndWn5JbRCPPtbeMqldXOn2Od17ybS92PHd1rNkZ80IMmOURHNct5YSVQ1MKBIDAyC+ck6VL7cVAfUA==} + engines: {node: ^20.0.0 || ^22.0.0 || ^24.0.0} + + '@sap/xssec@4.10.0': + resolution: {integrity: sha512-6SxDorJpQRNjI0sCTwHoFkyECI5IrxnRZ3adoTY0XeGM8QcMoYmLnJIPXoa6AYSzMB89rjJ02j1PQVWACYt/Hg==} + engines: {node: '>=18'} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -6587,6 +6638,9 @@ packages: resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} engines: {node: '>=18.0.0'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -7725,6 +7779,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@1.0.6': resolution: {integrity: sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==} @@ -8581,6 +8638,10 @@ packages: deprecated: Please use @electron/asar moving forward. There is no API change, just a package name change hasBin: true + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -8615,6 +8676,9 @@ packages: async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async-settle@1.0.0: resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==} engines: {node: '>= 0.10'} @@ -9364,15 +9428,27 @@ packages: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-convert@3.1.2: + resolution: {integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==} + engines: {node: '>=14.6'} + color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@2.0.2: + resolution: {integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==} + engines: {node: '>=12.20'} + color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-string@2.1.2: + resolution: {integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==} + engines: {node: '>=18'} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true @@ -9381,6 +9457,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + color@5.0.2: + resolution: {integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==} + engines: {node: '>=18'} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -9589,6 +9669,9 @@ packages: core-js@3.42.0: resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -10637,6 +10720,9 @@ packages: emoticon@4.1.0: resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -11100,6 +11186,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + fancy-log@1.3.3: resolution: {integrity: sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==} engines: {node: '>= 0.10'} @@ -11186,6 +11276,9 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + feed@4.2.2: resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} engines: {node: '>=0.4.0'} @@ -11334,6 +11427,9 @@ packages: flush-write-stream@1.1.1: resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -13334,6 +13430,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + ky@1.12.0: resolution: {integrity: sha512-YRLmSUHCwOJRBMArtqMRLOmO7fewn3yOoui6aB8ERkRVXupa0UiaQaKbIXteMt4jUElhbdqTMsLFHs8APxxUoQ==} engines: {node: '>=18'} @@ -13698,6 +13797,10 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + loglevel@1.9.2: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} @@ -14802,6 +14905,9 @@ packages: one-time@0.0.4: resolution: {integrity: sha512-qAMrwuk2xLEutlASoiPiAMW3EN3K96Ka/ilSXYr6qR1zSVXw2j7+yDSqGTC4T9apfLYxM3tLLjKvgPdAUK7kYQ==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -14855,6 +14961,10 @@ packages: resolution: {integrity: sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==} hasBin: true + opossum@9.0.0: + resolution: {integrity: sha512-K76U0QkxOfUZamneQuzz+AP0fyfTJcCplZ2oZL93nxeupuJbN4s6uFNbmVCt4eWqqGqRnnowdFuBicJ1fLMVxw==} + engines: {node: ^24 || ^22 || ^20} + option@0.2.4: resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} @@ -16817,6 +16927,10 @@ packages: safe-regex@1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -17740,6 +17854,9 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + textextensions@1.0.2: resolution: {integrity: sha512-jm9KjEWiDmtGLBrTqXEduGzlYTTlPaoDKdq5YRQhD0rYjo61ZNTYKZ/x5J4ajPSBH9wIYY5qm9GNG5otIKjtOA==} @@ -17931,6 +18048,10 @@ packages: resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} engines: {node: '>=12'} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + trough@1.0.5: resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} @@ -18527,6 +18648,10 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -18682,6 +18807,9 @@ packages: jsdom: optional: true + voca@1.4.1: + resolution: {integrity: sha512-NJC/BzESaHT1p4B5k4JykxedeltmNbau4cummStd4RjFojgq/kLew5TzYge9N2geeWyI2w8T30wUET5v+F7ZHA==} + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -18983,6 +19111,14 @@ packages: resolution: {integrity: sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==} engines: {node: '>=18'} + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.18.3: + resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==} + engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -19149,6 +19285,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -21198,13 +21339,13 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@chromatic-com/storybook@4.1.1(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@chromatic-com/storybook@4.1.1(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -21213,6 +21354,8 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@colors/colors@1.6.0': {} + '@corex/deepmerge@4.0.43': {} '@cspotcode/source-map-support@0.8.1': @@ -21506,6 +21649,12 @@ snapshots: dependencies: postcss: 8.5.4 + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + '@discoveryjs/json-ext@0.5.7': {} '@docsearch/css@3.9.0': {} @@ -22395,7 +22544,7 @@ snapshots: '@electron/get@2.0.3': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -22409,7 +22558,7 @@ snapshots: '@electron/get@4.0.2': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 env-paths: 3.0.0 got: 14.4.8 graceful-fs: 4.2.11 @@ -22684,7 +22833,7 @@ snapshots: '@antfu/install-pkg': 1.1.0 '@antfu/utils': 8.1.1 '@iconify/types': 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 globals: 15.15.0 kolorist: 1.8.0 local-pkg: 1.1.1 @@ -23115,21 +23264,21 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: glob: 10.4.5 magic-string: 0.27.0 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) optionalDependencies: typescript: 5.8.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: glob: 10.4.5 magic-string: 0.30.17 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) optionalDependencies: typescript: 5.8.3 @@ -23176,7 +23325,7 @@ snapshots: '@koa/router@13.1.1': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 http-errors: 2.0.0 koa-compose: 4.1.0 path-to-regexp: 6.3.0 @@ -23185,7 +23334,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -23846,7 +23995,7 @@ snapshots: '@puppeteer/browsers@2.10.5': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -23859,7 +24008,7 @@ snapshots: '@puppeteer/browsers@2.6.1': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -24638,6 +24787,124 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true + '@sap-ai-sdk/ai-api@2.1.0': + dependencies: + '@sap-ai-sdk/core': 2.1.0 + '@sap-cloud-sdk/connectivity': 4.1.2 + '@sap-cloud-sdk/util': 4.1.2 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-ai-sdk/core@2.1.0': + dependencies: + '@sap-cloud-sdk/connectivity': 4.1.2 + '@sap-cloud-sdk/http-client': 4.1.2 + '@sap-cloud-sdk/openapi': 4.1.2 + '@sap-cloud-sdk/util': 4.1.2 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-ai-sdk/foundation-models@2.1.0': + dependencies: + '@sap-ai-sdk/ai-api': 2.1.0 + '@sap-ai-sdk/core': 2.1.0 + '@sap-cloud-sdk/connectivity': 4.1.2 + '@sap-cloud-sdk/http-client': 4.1.2 + '@sap-cloud-sdk/util': 4.1.2 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-ai-sdk/orchestration@2.1.0': + dependencies: + '@sap-ai-sdk/ai-api': 2.1.0 + '@sap-ai-sdk/core': 2.1.0 + '@sap-ai-sdk/prompt-registry': 2.1.0 + '@sap-cloud-sdk/util': 4.1.2 + yaml: 2.8.1 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-ai-sdk/prompt-registry@2.1.0': + dependencies: + '@sap-ai-sdk/core': 2.1.0 + zod: 3.25.76 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-cloud-sdk/connectivity@4.1.2': + dependencies: + '@sap-cloud-sdk/resilience': 4.1.2 + '@sap-cloud-sdk/util': 4.1.2 + '@sap/xsenv': 6.0.0 + '@sap/xssec': 4.10.0 + async-retry: 1.3.3 + axios: 1.12.2 + jsonwebtoken: 9.0.2 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-cloud-sdk/http-client@4.1.2': + dependencies: + '@sap-cloud-sdk/connectivity': 4.1.2 + '@sap-cloud-sdk/resilience': 4.1.2 + '@sap-cloud-sdk/util': 4.1.2 + axios: 1.12.2 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-cloud-sdk/openapi@4.1.2': + dependencies: + '@sap-cloud-sdk/connectivity': 4.1.2 + '@sap-cloud-sdk/http-client': 4.1.2 + '@sap-cloud-sdk/resilience': 4.1.2 + '@sap-cloud-sdk/util': 4.1.2 + axios: 1.12.2 + transitivePeerDependencies: + - debug + - supports-color + + '@sap-cloud-sdk/resilience@4.1.2': + dependencies: + '@sap-cloud-sdk/util': 4.1.2 + async-retry: 1.3.3 + axios: 1.12.2 + opossum: 9.0.0 + transitivePeerDependencies: + - debug + + '@sap-cloud-sdk/util@4.1.2': + dependencies: + axios: 1.12.2 + chalk: 4.1.2 + logform: 2.7.0 + voca: 1.4.1 + winston: 3.18.3 + winston-transport: 4.9.0 + transitivePeerDependencies: + - debug + + '@sap/xsenv@6.0.0': + dependencies: + debug: 4.4.1(supports-color@8.1.1) + node-cache: 5.1.2 + verror: 1.10.1 + transitivePeerDependencies: + - supports-color + + '@sap/xssec@4.10.0': + dependencies: + debug: 4.4.3 + jwt-decode: 4.0.0 + transitivePeerDependencies: + - supports-color + '@sec-ant/readable-stream@0.4.1': {} '@sevinf/maybe@0.5.0': {} @@ -25252,6 +25519,11 @@ snapshots: '@smithy/util-buffer-from': 4.0.0 tslib: 2.8.1 + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.2 + text-hex: 1.0.0 + '@socket.io/component-emitter@3.1.2': {} '@standard-schema/utils@0.3.0': {} @@ -25292,15 +25564,15 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-docs@9.1.3(@types/react@18.3.23)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@storybook/addon-docs@9.1.3(@types/react@18.3.23)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@19.1.1) - '@storybook/csf-plugin': 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + '@storybook/csf-plugin': 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@19.1.1) - '@storybook/react-dom-shim': 9.1.3(react-dom@18.3.1(react@18.3.1))(react@19.1.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + '@storybook/react-dom-shim': 9.1.3(react-dom@18.3.1(react@18.3.1))(react@19.1.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) react: 19.1.1 react-dom: 18.3.1(react@19.1.1) - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -25335,10 +25607,10 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 - '@storybook/addon-links@9.1.3(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@storybook/addon-links@9.1.3(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) optionalDependencies: react: 18.3.1 @@ -25372,20 +25644,20 @@ snapshots: react: 19.1.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@storybook/builder-vite@8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@storybook/csf-plugin': 8.6.14(storybook@8.6.14(prettier@3.5.3)) browser-assert: 1.2.1 storybook: 8.6.14(prettier@3.5.3) ts-dedent: 2.2.0 - vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) - '@storybook/builder-vite@9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@storybook/builder-vite@9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: - '@storybook/csf-plugin': 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@storybook/csf-plugin': 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) '@storybook/components@8.6.14(storybook@8.6.14(prettier@3.5.3))': dependencies: @@ -25417,9 +25689,9 @@ snapshots: storybook: 8.6.14(prettier@3.5.3) unplugin: 1.16.1 - '@storybook/csf-plugin@9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@storybook/csf-plugin@9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -25455,23 +25727,23 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.6.14(prettier@3.5.3) - '@storybook/react-dom-shim@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@storybook/react-dom-shim@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) - '@storybook/react-dom-shim@9.1.3(react-dom@18.3.1(react@18.3.1))(react@19.1.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@storybook/react-dom-shim@9.1.3(react-dom@18.3.1(react@18.3.1))(react@19.1.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: react: 19.1.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) - '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@rollup/pluginutils': 5.2.0(rollup@4.40.2) - '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@3.5.3))(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) find-up: 5.0.0 magic-string: 0.30.17 @@ -25481,7 +25753,7 @@ snapshots: resolve: 1.22.10 storybook: 8.6.14(prettier@3.5.3) tsconfig-paths: 4.2.0 - vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) optionalDependencies: '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) transitivePeerDependencies: @@ -25489,21 +25761,21 @@ snapshots: - supports-color - typescript - '@storybook/react-vite@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@storybook/react-vite@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.2)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@rollup/pluginutils': 5.2.0(rollup@4.40.2) - '@storybook/builder-vite': 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) - '@storybook/react': 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.8.3) + '@storybook/builder-vite': 9.1.3(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + '@storybook/react': 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 8.0.1 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color @@ -25524,17 +25796,17 @@ snapshots: '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) typescript: 5.8.3 - '@storybook/react@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.8.3)': + '@storybook/react@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))) + '@storybook/react-dom-shim': 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) optionalDependencies: typescript: 5.8.3 - '@storybook/test-runner@0.23.0(@types/node@24.2.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)))': + '@storybook/test-runner@0.23.0(@types/node@24.2.1)(storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))': dependencies: '@babel/core': 7.27.1 '@babel/generator': 7.27.1 @@ -25554,7 +25826,7 @@ snapshots: jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@24.2.1)) nyc: 15.1.0 playwright: 1.55.0 - storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + storybook: 9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -25893,19 +26165,19 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@20.17.57)(typescript@5.8.3)) - '@tailwindcss/vite@4.1.6(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.6(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.1.6 '@tailwindcss/oxide': 4.1.6 tailwindcss: 4.1.6 - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) - '@tailwindcss/vite@4.1.6(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.6(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.1.6 '@tailwindcss/oxide': 4.1.6 tailwindcss: 4.1.6 - vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) '@tanstack/query-core@5.76.0': {} @@ -26553,6 +26825,8 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/triple-beam@1.3.5': {} + '@types/trusted-types@1.0.6': {} '@types/trusted-types@2.0.7': @@ -26643,7 +26917,7 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 eslint: 9.27.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: @@ -26722,25 +26996,25 @@ snapshots: '@use-gesture/core': 10.3.1 react: 18.3.1 - '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -26775,13 +27049,21 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) '@vitest/pretty-format@2.0.5': dependencies: @@ -26824,7 +27106,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) '@vitest/utils@2.0.5': dependencies: @@ -27646,6 +27928,8 @@ snapshots: optionalDependencies: '@types/glob': 7.2.0 + assert-plus@1.0.0: {} + assertion-error@2.0.1: {} assign-symbols@1.0.0: {} @@ -27675,6 +27959,10 @@ snapshots: dependencies: tslib: 2.8.1 + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + async-settle@1.0.0: dependencies: async-done: 1.3.2 @@ -27941,7 +28229,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -28590,16 +28878,26 @@ snapshots: dependencies: color-name: 1.1.4 + color-convert@3.1.2: + dependencies: + color-name: 2.0.2 + color-name@1.1.3: {} color-name@1.1.4: {} + color-name@2.0.2: {} + color-string@1.9.1: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 optional: true + color-string@2.1.2: + dependencies: + color-name: 2.0.2 + color-support@1.1.3: {} color@4.2.3: @@ -28608,6 +28906,11 @@ snapshots: color-string: 1.9.1 optional: true + color@5.0.2: + dependencies: + color-convert: 3.1.2 + color-string: 2.1.2 + colord@2.9.3: {} colorette@2.0.20: {} @@ -28798,6 +29101,8 @@ snapshots: core-js@3.42.0: {} + core-util-is@1.0.2: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -29824,6 +30129,8 @@ snapshots: emoticon@4.1.0: {} + enabled@2.0.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -30584,7 +30891,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -30592,6 +30899,8 @@ snapshots: transitivePeerDependencies: - supports-color + extsprintf@1.4.1: {} + fancy-log@1.3.3: dependencies: ansi-gray: 0.1.1 @@ -30678,6 +30987,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fecha@4.2.3: {} + feed@4.2.2: dependencies: xml-js: 1.6.11 @@ -30749,7 +31060,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -30866,6 +31177,8 @@ snapshots: inherits: 2.0.4 readable-stream: 2.3.8 + fn.name@1.1.0: {} + follow-redirects@1.15.9(debug@4.4.1): optionalDependencies: debug: 4.4.1(supports-color@8.1.1) @@ -33449,7 +33762,7 @@ snapshots: koa-mount@4.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 koa-compose: 4.1.0 transitivePeerDependencies: - supports-color @@ -33476,7 +33789,7 @@ snapshots: content-disposition: 0.5.4 content-type: 1.0.5 cookies: 0.9.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 delegates: 1.0.0 depd: 2.0.0 destroy: 1.2.0 @@ -33499,6 +33812,8 @@ snapshots: kolorist@1.8.0: {} + kuler@2.0.0: {} + ky@1.12.0: {} langium@3.3.1: @@ -33837,6 +34152,15 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.0 + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + loglevel@1.9.2: {} longest-streak@3.1.0: {} @@ -35413,6 +35737,10 @@ snapshots: one-time@0.0.4: {} + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -35468,6 +35796,8 @@ snapshots: dependencies: tiny-inflate: 1.0.3 + opossum@9.0.0: {} + option@0.2.4: {} optionator@0.9.4: @@ -36170,6 +36500,15 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.4 + tsx: 4.19.4 + yaml: 2.8.1 + postcss-loader@7.3.4(postcss@8.5.4)(typescript@5.6.3)(webpack@5.101.3(esbuild@0.25.9)): dependencies: cosmiconfig: 8.3.6(typescript@5.6.3) @@ -36653,7 +36992,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -36753,7 +37092,7 @@ snapshots: dependencies: '@puppeteer/browsers': 2.10.5 chromium-bidi: 5.1.0(devtools-protocol@0.0.1452169) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 devtools-protocol: 0.0.1452169 typed-query-selector: 2.12.0 ws: 8.18.3 @@ -37684,7 +38023,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -37746,6 +38085,8 @@ snapshots: dependencies: ret: 0.1.15 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sanitize-filename@1.6.3: @@ -37846,7 +38187,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -38212,7 +38553,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -38332,7 +38673,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -38431,13 +38772,13 @@ snapshots: - supports-color - utf-8-validate - storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)): + storybook@9.1.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.3 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.9 @@ -38716,7 +39057,7 @@ snapshots: sumchecker@3.0.1: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -38923,6 +39264,8 @@ snapshots: dependencies: b4a: 1.6.7 + text-hex@1.0.0: {} + textextensions@1.0.2: {} thenify-all@1.6.0: @@ -39093,6 +39436,8 @@ snapshots: trim-newlines@4.1.1: {} + triple-beam@1.4.1: {} + trough@1.0.5: {} trough@2.2.0: {} @@ -39230,6 +39575,35 @@ snapshots: - tsx - yaml + tsup@8.5.0(@swc/core@1.13.5)(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.9) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1(supports-color@8.1.1) + esbuild: 0.25.9 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.4)(tsx@4.19.4)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.40.2 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.13.5 + postcss: 8.5.4 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.19.4: dependencies: esbuild: 0.25.9 @@ -39741,6 +40115,12 @@ snapshots: - '@types/react' - '@types/react-dom' + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -39889,13 +40269,34 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.4(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-node@3.2.4(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.6(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.6(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -39944,7 +40345,24 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.40.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.17.57 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + terser: 5.43.1 + tsx: 4.19.4 + yaml: 2.8.1 + + vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.4.6(picomatch@4.0.2) @@ -39959,7 +40377,7 @@ snapshots: lightningcss: 1.30.1 terser: 5.43.1 tsx: 4.19.4 - yaml: 2.8.0 + yaml: 2.8.1 vite@6.3.6(@types/node@20.17.50)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: @@ -39995,7 +40413,24 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vite@6.3.6(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.6(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.40.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.17.57 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + terser: 5.43.1 + tsx: 4.19.4 + yaml: 2.8.1 + + vite@6.3.6(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.4.6(picomatch@4.0.2) @@ -40010,7 +40445,7 @@ snapshots: lightningcss: 1.30.1 terser: 5.43.1 tsx: 4.19.4 - yaml: 2.8.0 + yaml: 2.8.1 vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: @@ -40100,11 +40535,55 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1(supports-color@8.1.1) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@20.17.57)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.17.57 + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -40122,8 +40601,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.2.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -40144,6 +40623,8 @@ snapshots: - tsx - yaml + voca@1.4.1: {} + void-elements@3.1.0: {} vscode-jsonrpc@8.2.0: {} @@ -40561,6 +41042,26 @@ snapshots: dependencies: execa: 8.0.1 + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.18.3: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -40667,6 +41168,8 @@ snapshots: yaml@2.8.0: {} + yaml@2.8.1: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/src/api/index.ts b/src/api/index.ts index 029bb50e1c2..c9d39dc256a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -47,6 +47,7 @@ import { VercelAiGatewayHandler, DeepInfraHandler, OVHcloudAIEndpointsHandler, // kilocode_change + SapAiCoreHandler, } from "./providers" // kilocode_change start import { KilocodeOpenrouterHandler } from "./providers/kilocode-openrouter" @@ -206,6 +207,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { case "ovhcloud": return new OVHcloudAIEndpointsHandler(options) // kilocode_change end + case "sap-ai-core": + return new SapAiCoreHandler(options) default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/__tests__/sap-ai-core.spec.ts b/src/api/providers/__tests__/sap-ai-core.spec.ts new file mode 100644 index 00000000000..9f25659fd85 --- /dev/null +++ b/src/api/providers/__tests__/sap-ai-core.spec.ts @@ -0,0 +1,692 @@ +// npx vitest run api/providers/__tests__/sap-ai-core.spec.ts + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { Anthropic } from "@anthropic-ai/sdk" +// Import after mocks are set up +import { SapAiCoreHandler } from "../sap-ai-core" +import { ApiHandlerOptions } from "../../../shared/api" +import { getProviderForModel, getSapAiCoreModels } from "../fetchers/sap-ai-core" +import { OrchestrationClient } from "@sap-ai-sdk/orchestration" +import { AzureOpenAiChatClient } from "@sap-ai-sdk/foundation-models" + +// Mock SAP AI SDK modules +vi.mock("@sap-ai-sdk/orchestration", () => ({ + OrchestrationClient: vi.fn(), +})) + +vi.mock("@sap-ai-sdk/foundation-models", () => ({ + AzureOpenAiChatClient: vi.fn(), +})) + +// Mock the SAP AI Core fetcher +vi.mock("../fetchers/sap-ai-core", () => ({ + getSapAiCoreModels: vi.fn(), + getProviderForModel: vi.fn(), +})) + +describe("SapAiCoreHandler", () => { + let handler: SapAiCoreHandler + let mockOptions: ApiHandlerOptions + + // Mock functions + const mockStreamToContentStream = vi.fn() + const mockGetTokenUsage = vi.fn() + const mockGetContent = vi.fn() + const mockChatCompletion = vi.fn() + const mockStream = vi.fn() + const mockRun = vi.fn() + + beforeEach(() => { + const testServiceKey = { + clientid: "test-client-id", + clientsecret: "test-client-secret", + url: "https://test-auth-url.com", + serviceurls: { + AI_API_URL: "https://test-base-url.com", + }, + } + + mockOptions = { + sapAiCoreServiceKey: JSON.stringify(testServiceKey), + sapAiCoreResourceGroup: "test-resource-group", + sapAiCoreModelId: "gpt-4o-mini", + sapAiCoreUseOrchestration: false, + } + + // Reset all mocks + vi.clearAllMocks() + + // Setup default mock responses - now returns ModelInfo directly + vi.mocked(getSapAiCoreModels).mockResolvedValue({ + "gpt-4o-mini": { + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.00015, + outputPrice: 0.0006, + description: "GPT-4o mini model", + displayName: "GPT-4o Mini", + preferredIndex: undefined, + }, + }) + + // Setup mock for getProviderForModel to return "OpenAI" by default + vi.mocked(getProviderForModel).mockReturnValue("OpenAI") + + // Setup mock stream responses + mockStreamToContentStream.mockImplementation(async function* () { + yield "Test response from SAP AI Core" + }) + + mockGetTokenUsage.mockReturnValue({ + prompt_tokens: 10, + completion_tokens: 15, + total_tokens: 25, + }) + + mockGetContent.mockReturnValue("Test response from SAP AI Core") + + const mockStreamResponse = { + stream: { + toContentStream: mockStreamToContentStream, + }, + getTokenUsage: mockGetTokenUsage, + } + + const mockRunResponse = { + stream: { + toContentStream: mockStreamToContentStream, + }, + getTokenUsage: mockGetTokenUsage, + getContent: mockGetContent, + } + + mockStream.mockResolvedValue(mockStreamResponse) + mockRun.mockResolvedValue(mockRunResponse) + mockChatCompletion.mockResolvedValue(mockRunResponse) + + // Setup mock constructors + const mockOrchestrationClient = { + stream: mockStream, + chatCompletion: mockChatCompletion, + } + + const mockAzureOpenAiChatClient = { + stream: mockStream, + run: mockRun, + } + + vi.mocked(OrchestrationClient).mockImplementation(() => mockOrchestrationClient as any) + vi.mocked(AzureOpenAiChatClient).mockImplementation(() => mockAzureOpenAiChatClient as any) + + handler = new SapAiCoreHandler(mockOptions) + }) + + afterEach(() => { + vi.restoreAllMocks() + // Clean up environment variable + delete process.env["AICORE_SERVICE_KEY"] + }) + + describe("constructor", () => { + it("should initialize with provided options", () => { + expect(handler).toBeInstanceOf(SapAiCoreHandler) + expect(handler.getModel().id).toBe(mockOptions.sapAiCoreModelId) + }) + + it("should default to foundation backend when useOrchestration is false", () => { + const foundationHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: false, + }) + expect(foundationHandler).toBeInstanceOf(SapAiCoreHandler) + }) + + it("should use orchestration backend when useOrchestration is true", () => { + const orchestrationHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: true, + }) + expect(orchestrationHandler).toBeInstanceOf(SapAiCoreHandler) + }) + + it("should default useOrchestration to false when not specified", () => { + const defaultHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: undefined, + }) + expect(defaultHandler).toBeInstanceOf(SapAiCoreHandler) + }) + }) + + describe("getModel", () => { + it("should return model info for foundation models", () => { + const model = handler.getModel() + expect(model.id).toBe("gpt-4o-mini") + expect(model.info).toBeDefined() + expect(model.info?.contextWindow).toBe(32768) + expect(model.info?.supportsImages).toBe(false) + expect(model.info?.inputPrice).toBe(0.00015) + expect(model.info?.outputPrice).toBe(0.0006) + }) + + it("should return model info for orchestration models", async () => { + vi.mocked(getSapAiCoreModels).mockResolvedValue({ + "gpt-4o-orchestration": { + contextWindow: 32768, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.005, + outputPrice: 0.015, + description: "GPT-4o orchestration model", + displayName: "GPT-4o Orchestration", + preferredIndex: undefined, + }, + }) + + const orchestrationHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreModelId: "gpt-4o-orchestration", + sapAiCoreUseOrchestration: true, + }) + + // Wait for initialization to complete + await new Promise((resolve) => setTimeout(resolve, 0)) + + const model = orchestrationHandler.getModel() + expect(model.id).toBe("gpt-4o-orchestration") + expect(model.info?.supportsImages).toBe(true) + expect(model.info?.inputPrice).toBe(0.005) + }) + }) + + describe("environment setup", () => { + it("should set up AICORE_SERVICE_KEY environment variable", async () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + // Consume the stream to trigger environment setup + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task-1" }) + await stream.next() + + expect(process.env["AICORE_SERVICE_KEY"]).toBeDefined() + const serviceKey = JSON.parse(process.env["AICORE_SERVICE_KEY"]!) + expect(serviceKey).toEqual({ + clientid: "test-client-id", + clientsecret: "test-client-secret", + url: "https://test-auth-url.com", + serviceurls: { + AI_API_URL: "https://test-base-url.com", + }, + }) + }) + + it("should only set up environment once", async () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + // First call + const stream1 = handler.createMessage(systemPrompt, messages, { taskId: "test-task-2" }) + await stream1.next() + + const firstServiceKey = process.env["AICORE_SERVICE_KEY"] + + // Second call + const stream2 = handler.createMessage(systemPrompt, messages, { taskId: "test-task-3" }) + await stream2.next() + + const secondServiceKey = process.env["AICORE_SERVICE_KEY"] + + expect(firstServiceKey).toBe(secondServiceKey) + }) + }) + + describe("createMessage with Foundation backend", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + it("should handle streaming responses successfully", async () => { + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task-4" }) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunks = chunks.filter((chunk) => chunk.type === "text") + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("Test response from SAP AI Core") + + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 15, + cacheWriteTokens: undefined, + cacheReadTokens: undefined, + }) + + expect(vi.mocked(AzureOpenAiChatClient)).toHaveBeenCalled() + }) + + it("should handle complex message content", async () => { + const complexMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "What is in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data", + }, + }, + ], + }, + ] + + const stream = handler.createMessage(systemPrompt, complexMessages, { taskId: "test-task-5" }) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + expect(vi.mocked(AzureOpenAiChatClient)).toHaveBeenCalled() + }) + + it("should handle temperature parameter", async () => { + const handlerWithTemp = new SapAiCoreHandler({ + ...mockOptions, + modelTemperature: 0.7, + }) + + const stream = handlerWithTemp.createMessage(systemPrompt, messages, { taskId: "test-task-6" }) + await stream.next() + + expect(vi.mocked(AzureOpenAiChatClient)).toHaveBeenCalled() + }) + + it("should handle max tokens parameter", async () => { + const handlerWithMaxTokens = new SapAiCoreHandler({ + ...mockOptions, + modelMaxTokens: 2048, + }) + + const stream = handlerWithMaxTokens.createMessage(systemPrompt, messages, { taskId: "test-task-7" }) + await stream.next() + + expect(vi.mocked(AzureOpenAiChatClient)).toHaveBeenCalled() + }) + + it("should handle deployment ID parameter", async () => { + const handlerWithDeployment = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreDeploymentId: "test-deployment-123", + }) + + const stream = handlerWithDeployment.createMessage(systemPrompt, messages, { taskId: "test-task-8" }) + await stream.next() + + expect(vi.mocked(AzureOpenAiChatClient)).toHaveBeenCalled() + }) + }) + + describe("createMessage with Orchestration backend", () => { + let orchestrationHandler: SapAiCoreHandler + + beforeEach(() => { + orchestrationHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: true, + }) + }) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + it("should handle streaming responses successfully", async () => { + const stream = orchestrationHandler.createMessage(systemPrompt, messages, { taskId: "test-task-9" }) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunks = chunks.filter((chunk) => chunk.type === "text") + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("Test response from SAP AI Core") + + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 15, + cacheWriteTokens: undefined, + cacheReadTokens: undefined, + }) + + expect(vi.mocked(OrchestrationClient)).toHaveBeenCalled() + }) + + it("should handle temperature parameter", async () => { + const orchestrationHandlerWithTemp = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: true, + modelTemperature: 0.8, + }) + + const stream = orchestrationHandlerWithTemp.createMessage(systemPrompt, messages, { + taskId: "test-task-10", + }) + await stream.next() + + expect(vi.mocked(OrchestrationClient)).toHaveBeenCalled() + }) + + it("should handle max tokens parameter", async () => { + const orchestrationHandlerWithMaxTokens = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: true, + modelMaxTokens: 4096, + }) + + const stream = orchestrationHandlerWithMaxTokens.createMessage(systemPrompt, messages, { + taskId: "test-task-11", + }) + await stream.next() + + expect(vi.mocked(OrchestrationClient)).toHaveBeenCalled() + }) + }) + + describe("completePrompt", () => { + it("should complete prompt successfully with foundation backend", async () => { + const result = await handler.completePrompt("What is the weather like?") + + expect(result).toBe("Test response from SAP AI Core") + expect(vi.mocked(AzureOpenAiChatClient)).toHaveBeenCalled() + }) + + it("should complete prompt successfully with orchestration backend", async () => { + const orchestrationHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: true, + }) + + const result = await orchestrationHandler.completePrompt("What is the weather like?") + + expect(result).toBe("Test response from SAP AI Core") + expect(vi.mocked(OrchestrationClient)).toHaveBeenCalled() + }) + + it("should handle empty response", async () => { + mockGetContent.mockReturnValue("") + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("") + }) + + it("should handle API errors", async () => { + mockRun.mockRejectedValueOnce(new Error("API Error")) + await expect(handler.completePrompt("Test prompt")).rejects.toThrow( + "SAP AI Core completion error: API Error", + ) + }) + }) + + describe("error handling", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + it("should handle streaming errors in foundation backend", async () => { + // Mock the stream method to reject with an error + mockStream.mockRejectedValueOnce(new Error("Foundation API Error")) + + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task-12" }) + + await expect(async () => { + for await (const _chunk of stream) { + // Should not reach here + } + }).rejects.toThrow("Foundation API Error") + }) + + it("should handle streaming errors in orchestration backend", async () => { + const orchestrationHandler = new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: true, + }) + + mockStream.mockRejectedValueOnce(new Error("Orchestration API Error")) + + const stream = orchestrationHandler.createMessage(systemPrompt, messages, { taskId: "test-task-13" }) + + await expect(async () => { + for await (const _chunk of stream) { + // Should not reach here + } + }).rejects.toThrow("Orchestration API Error") + }) + + it("should handle unsupported provider in foundation backend", () => { + // Since we now only store ModelInfo and assume OpenAI for foundation models, + // this test is no longer relevant as provider detection is simplified. + // We'll keep it but expect it to not throw since we always assume OpenAI + vi.mocked(getSapAiCoreModels).mockResolvedValue({ + "anthropic-model": { + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.001, + outputPrice: 0.003, + description: "Anthropic model", + displayName: "Anthropic Model", + preferredIndex: undefined, + }, + }) + + // This should not throw anymore since we assume OpenAI for all foundation models + expect( + () => + new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreModelId: "anthropic-model", + sapAiCoreUseOrchestration: false, + }), + ).not.toThrow() + }) + }) + + describe("backend routing", () => { + it("should route to OpenAI foundation backend for OpenAI models", () => { + // This should not throw since OpenAI is supported + expect( + () => + new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreUseOrchestration: false, + }), + ).not.toThrow() + }) + + it("should not throw for any foundation models (simplified provider handling)", () => { + vi.mocked(getSapAiCoreModels).mockResolvedValue({ + "google-model": { + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.001, + outputPrice: 0.003, + description: "Google model", + displayName: "Google Model", + preferredIndex: undefined, + }, + }) + + // Should not throw since we simplified provider detection + expect( + () => + new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreModelId: "google-model", + sapAiCoreUseOrchestration: false, + }), + ).not.toThrow() + }) + + it("should not throw for Amazon foundation models (simplified provider handling)", () => { + vi.mocked(getSapAiCoreModels).mockResolvedValue({ + "amazon-model": { + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.001, + outputPrice: 0.003, + description: "Amazon model", + displayName: "Amazon Model", + preferredIndex: undefined, + }, + }) + + // Should not throw since we simplified provider detection + expect( + () => + new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreModelId: "amazon-model", + sapAiCoreUseOrchestration: false, + }), + ).not.toThrow() + }) + + it("should not throw for Mistral foundation models (simplified provider handling)", () => { + vi.mocked(getSapAiCoreModels).mockResolvedValue({ + "mistral-model": { + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.001, + outputPrice: 0.003, + description: "Mistral model", + displayName: "Mistral Model", + preferredIndex: undefined, + }, + }) + + // Should not throw since we simplified provider detection + expect( + () => + new SapAiCoreHandler({ + ...mockOptions, + sapAiCoreModelId: "mistral-model", + sapAiCoreUseOrchestration: false, + }), + ).not.toThrow() + }) + }) + + describe("token usage handling", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + it("should handle missing token usage gracefully", async () => { + mockGetTokenUsage.mockReturnValue(null) + + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task-14" }) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks).toHaveLength(0) + }) + + it("should handle partial token usage", async () => { + mockGetTokenUsage.mockReturnValue({ + prompt_tokens: 5, + // Missing completion_tokens + }) + + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task-15" }) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0]).toEqual({ + type: "usage", + inputTokens: 5, + outputTokens: 0, + cacheWriteTokens: undefined, + cacheReadTokens: undefined, + }) + }) + + it("should handle cache token usage", async () => { + mockGetTokenUsage.mockReturnValue({ + prompt_tokens: 10, + completion_tokens: 15, + cache_creation_input_tokens: 5, + cache_read_input_tokens: 3, + }) + + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task-16" }) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks).toHaveLength(1) + expect(usageChunks[0]).toEqual({ + type: "usage", + inputTokens: 10, + outputTokens: 15, + cacheWriteTokens: 5, + cacheReadTokens: 3, + }) + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-deployments.json b/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-deployments.json new file mode 100644 index 00000000000..d4c7886bb9d --- /dev/null +++ b/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-deployments.json @@ -0,0 +1,58 @@ +[ + { + "scope": "https://test-auth-url.com:443", + "method": "POST", + "path": "/oauth/token", + "body": { + "grant_type": "client_credentials", + "client_id": "test-client-id", + "client_secret": "test-client-secret" + }, + "status": 200, + "response": { + "access_token": "test-access-token", + "expires_in": 3600, + "scope": "test", + "jti": "test-jti", + "token_type": "Bearer" + } + }, + { + "scope": "https://test-base-url.com:443", + "method": "GET", + "path": "/v2/lm/deployments?$top=10000&$skip=0", + "status": 200, + "response": { + "resources": [ + { + "id": "test-deployment-1", + "targetStatus": "RUNNING", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "gpt-4o-mini", + "version": "1.0.0" + } + } + } + } + }, + { + "id": "test-deployment-2", + "targetStatus": "STOPPED", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "gpt-4o", + "version": "2.0.0" + } + } + } + } + } + ] + } + } +] diff --git a/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-models-orchestration.json b/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-models-orchestration.json new file mode 100644 index 00000000000..b6d73139d43 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-models-orchestration.json @@ -0,0 +1,66 @@ +[ + { + "scope": "https://test-auth-url.com:443", + "method": "POST", + "path": "/oauth/token", + "body": { + "grant_type": "client_credentials", + "client_id": "test-client-id", + "client_secret": "test-client-secret" + }, + "status": 200, + "response": { + "access_token": "test-access-token", + "expires_in": 3600, + "scope": "test", + "jti": "test-jti", + "token_type": "Bearer" + } + }, + { + "scope": "https://test-base-url.com:443", + "method": "GET", + "path": "/v2/lm/deployments?$top=10000&$skip=0", + "status": 200, + "response": { + "resources": [] + } + }, + { + "scope": "https://test-base-url.com:443", + "method": "GET", + "path": "/v2/lm/scenarios/foundation-models/models", + "status": 200, + "response": { + "resources": [ + { + "model": "gpt-4o-orchestration", + "provider": "OpenAI", + "accessType": "Remote", + "description": "GPT-4o model with orchestration support", + "displayName": "GPT-4o Orchestration", + "allowedScenarios": [ + { + "scenarioId": "orchestration" + } + ], + "versions": [ + { + "isLatest": true, + "streamingSupported": true, + "capabilities": ["text-generation", "image-recognition"], + "contextLength": 32768, + "inputTypes": ["text", "image"], + "cost": [ + { + "inputCost": "0.005000", + "outputCost": "0.015000" + } + ] + } + ] + } + ] + } + } +] diff --git a/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-models.json b/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-models.json new file mode 100644 index 00000000000..7cd705bf8b7 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/fixtures/sap-ai-core-models.json @@ -0,0 +1,66 @@ +[ + { + "scope": "https://test-auth-url.com:443", + "method": "POST", + "path": "/oauth/token", + "body": { + "grant_type": "client_credentials", + "client_id": "test-client-id", + "client_secret": "test-client-secret" + }, + "status": 200, + "response": { + "access_token": "test-access-token", + "expires_in": 3600, + "scope": "test", + "jti": "test-jti", + "token_type": "Bearer" + } + }, + { + "scope": "https://test-base-url.com:443", + "method": "GET", + "path": "/v2/lm/deployments?$top=10000&$skip=0", + "status": 200, + "response": { + "resources": [] + } + }, + { + "scope": "https://test-base-url.com:443", + "method": "GET", + "path": "/v2/lm/scenarios/foundation-models/models", + "status": 200, + "response": { + "resources": [ + { + "model": "gpt-4o-mini", + "provider": "OpenAI", + "accessType": "Remote", + "description": "GPT-4o mini model for testing", + "displayName": "GPT-4o Mini", + "allowedScenarios": [ + { + "scenarioId": "foundation-models" + } + ], + "versions": [ + { + "isLatest": true, + "streamingSupported": true, + "capabilities": ["text-generation"], + "contextLength": 32768, + "inputTypes": ["text"], + "cost": [ + { + "inputCost": "0.000150", + "outputCost": "0.000600" + } + ] + } + ] + } + ] + } + } +] diff --git a/src/api/providers/fetchers/__tests__/sap-ai-core.spec.ts b/src/api/providers/fetchers/__tests__/sap-ai-core.spec.ts new file mode 100644 index 00000000000..83669c09aee --- /dev/null +++ b/src/api/providers/fetchers/__tests__/sap-ai-core.spec.ts @@ -0,0 +1,435 @@ +// npx vitest run api/providers/fetchers/__tests__/sap-ai-core.spec.ts + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import * as path from "path" +import { back as nockBack } from "nock" +import * as sapAiCore from "../sap-ai-core" +import axios, { AxiosResponse } from "axios" + +nockBack.fixtures = path.join(__dirname, "fixtures") +nockBack.setMode("lockdown") + +// Mock service key for testing +const mockServiceKey = JSON.stringify({ + clientid: "test-client-id", + clientsecret: "test-client-secret", + url: "https://test-auth-url.com", + serviceurls: { + AI_API_URL: "https://test-base-url.com", + }, +}) + +describe("SAP AI Core API", () => { + describe("getSapAiCoreModels - with fixtures", () => { + it("fetches models and validates schema", async () => { + const { nockDone } = await nockBack("sap-ai-core-models.json") + + const models = await sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group") + + expect(models).toBeDefined() + expect(Object.keys(models).length).toBeGreaterThan(0) + + // Check a specific model (adjust based on your actual data) + const sampleModel = models["gpt-4o-mini"] + if (sampleModel) { + expect(sampleModel).toEqual({ + contextWindow: expect.any(Number), + supportsImages: expect.any(Boolean), + supportsPromptCache: expect.any(Boolean), + inputPrice: expect.any(Number), + outputPrice: expect.any(Number), + description: expect.any(String), + displayName: expect.any(String), + preferredIndex: undefined, + }) + } + + nockDone() + }) + + it("fetches models with useOrchestration set to true", async () => { + const { nockDone } = await nockBack("sap-ai-core-models-orchestration.json") + + const models = await sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group", true) + + expect(models).toBeDefined() + expect(Object.keys(models).length).toBeGreaterThanOrEqual(0) + + nockDone() + }) + + it("uses default resource group when not provided", async () => { + const { nockDone } = await nockBack("sap-ai-core-models.json") + + const models = await sapAiCore.getSapAiCoreModels(mockServiceKey) + + expect(models).toBeDefined() + nockDone() + }) + + it("throws error when service key is not provided", async () => { + await expect(sapAiCore.getSapAiCoreModels(undefined)).rejects.toThrow("SAP AI Core service key is required") + }) + + it("throws error when service key is invalid JSON", async () => { + await expect(sapAiCore.getSapAiCoreModels("invalid-json")).rejects.toThrow( + "Failed to parse SAP AI Core service key", + ) + }) + }) + + describe("getSapAiCoreDeployments - with fixtures", () => { + it("fetches deployments and validates schema", async () => { + const { nockDone } = await nockBack("sap-ai-core-deployments.json") + + const deployments = await sapAiCore.getSapAiCoreDeployments(mockServiceKey, "test-resource-group") + + expect(deployments).toBeDefined() + expect(Object.keys(deployments).length).toBeGreaterThanOrEqual(0) + + // Check a specific deployment if it exists + const deploymentKeys = Object.keys(deployments) + if (deploymentKeys.length > 0) { + const sampleDeployment = deployments[deploymentKeys[0]] + expect(sampleDeployment).toEqual({ + id: expect.any(String), + name: expect.any(String), + model: expect.any(String), + targetStatus: expect.any(String), + }) + } + + nockDone() + }) + + it("uses default resource group when not provided", async () => { + const { nockDone } = await nockBack("sap-ai-core-deployments.json") + + const deployments = await sapAiCore.getSapAiCoreDeployments(mockServiceKey) + + expect(deployments).toBeDefined() + nockDone() + }) + + it("throws error when service key is not provided", async () => { + await expect(sapAiCore.getSapAiCoreDeployments(undefined)).rejects.toThrow( + "SAP AI Core service key is required", + ) + }) + }) + + describe("Model Provider Caching", () => { + beforeEach(() => { + // Set nock to wild mode to disable interception for mocked tests + nockBack.setMode("wild") + vi.resetAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + // Reset nock back to lockdown mode + nockBack.setMode("lockdown") + }) + + it("populates cache after fetching models", async () => { + // Mock successful authentication + const axiosPostSpy = vi.spyOn(axios, "post").mockResolvedValueOnce({ + data: { access_token: "test-token", expires_in: 3600 }, + status: 200, + statusText: "OK", + headers: {}, + config: {}, + } as AxiosResponse) + + // Mock successful models and deployments API calls + const mockModelsResponse = { + data: { + resources: [ + { + model: "gpt-4", + provider: "OpenAI", + allowedScenarios: [{ scenarioId: "foundation-models" }], + versions: [ + { + isLatest: true, + streamingSupported: true, + capabilities: ["text-generation"], + contextLength: 32000, + cost: [{ inputCost: "0.01" }, { outputCost: "0.03" }], + }, + ], + }, + ], + }, + } + + const mockDeploymentsResponse = { + data: { resources: [] }, + } + + const axiosGetSpy = vi + .spyOn(axios, "get") + .mockResolvedValueOnce(mockModelsResponse) + .mockResolvedValueOnce(mockDeploymentsResponse) + + // Fetch models to populate cache + await sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group") + + // Verify cache is populated + const provider = sapAiCore.getProviderForModel("gpt-4") + expect(provider).toBe("OpenAI") + + expect(axiosPostSpy).toHaveBeenCalledOnce() + expect(axiosGetSpy).toHaveBeenCalledTimes(2) + }) + + it("clears cache when fetching new models", async () => { + // First, populate cache with one model + const axiosPostSpy = vi.spyOn(axios, "post").mockResolvedValue({ + data: { access_token: "test-token", expires_in: 3600 }, + status: 200, + statusText: "OK", + headers: {}, + config: {}, + } as AxiosResponse) + + const firstModelsResponse = { + data: { + resources: [ + { + model: "claude-3", + provider: "Anthropic", + allowedScenarios: [{ scenarioId: "foundation-models" }], + versions: [ + { + isLatest: true, + streamingSupported: true, + capabilities: ["text-generation"], + contextLength: 32000, + cost: [{ inputCost: "0.01" }, { outputCost: "0.03" }], + }, + ], + }, + ], + }, + } + + const mockDeploymentsResponse = { + data: { resources: [] }, + } + + let axiosGetSpy = vi + .spyOn(axios, "get") + .mockResolvedValueOnce(firstModelsResponse) + .mockResolvedValueOnce(mockDeploymentsResponse) + + await sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group") + + // Verify first model is cached + expect(sapAiCore.getProviderForModel("claude-3")).toBe("Anthropic") + + // Now fetch different models (should clear cache) + const secondModelsResponse = { + data: { + resources: [ + { + model: "gpt-4", + provider: "OpenAI", + allowedScenarios: [{ scenarioId: "foundation-models" }], + versions: [ + { + isLatest: true, + streamingSupported: true, + capabilities: ["text-generation"], + contextLength: 32000, + cost: [{ inputCost: "0.01" }, { outputCost: "0.03" }], + }, + ], + }, + ], + }, + } + + axiosGetSpy = vi + .spyOn(axios, "get") + .mockResolvedValueOnce(secondModelsResponse) + .mockResolvedValueOnce(mockDeploymentsResponse) + + await sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group") + + // Verify cache was cleared and repopulated + expect(sapAiCore.getProviderForModel("claude-3")).toBeUndefined() // Old model should be gone + expect(sapAiCore.getProviderForModel("gpt-4")).toBe("OpenAI") // New model should be present + }) + + it("handles multiple models in cache", async () => { + // Mock successful authentication + const axiosPostSpy = vi.spyOn(axios, "post").mockResolvedValueOnce({ + data: { access_token: "test-token", expires_in: 3600 }, + status: 200, + statusText: "OK", + headers: {}, + config: {}, + } as AxiosResponse) + + // Mock multiple models response + const mockModelsResponse = { + data: { + resources: [ + { + model: "gpt-4", + provider: "OpenAI", + allowedScenarios: [{ scenarioId: "foundation-models" }], + versions: [ + { + isLatest: true, + streamingSupported: true, + capabilities: ["text-generation"], + contextLength: 32000, + cost: [{ inputCost: "0.01" }, { outputCost: "0.03" }], + }, + ], + }, + { + model: "claude-3-sonnet", + provider: "Anthropic", + allowedScenarios: [{ scenarioId: "foundation-models" }], + versions: [ + { + isLatest: true, + streamingSupported: true, + capabilities: ["text-generation"], + contextLength: 32000, + cost: [{ inputCost: "0.01" }, { outputCost: "0.03" }], + }, + ], + }, + { + model: "gemini-pro", + provider: "Google", + allowedScenarios: [{ scenarioId: "foundation-models" }], + versions: [ + { + isLatest: true, + streamingSupported: true, + capabilities: ["text-generation"], + contextLength: 32000, + cost: [{ inputCost: "0.01" }, { outputCost: "0.03" }], + }, + ], + }, + ], + }, + } + + const mockDeploymentsResponse = { + data: { resources: [] }, + } + + const axiosGetSpy = vi + .spyOn(axios, "get") + .mockResolvedValueOnce(mockModelsResponse) + .mockResolvedValueOnce(mockDeploymentsResponse) + + // Fetch models to populate cache + await sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group") + + // Verify all models are cached with correct providers + expect(sapAiCore.getProviderForModel("gpt-4")).toBe("OpenAI") + expect(sapAiCore.getProviderForModel("claude-3-sonnet")).toBe("Anthropic") + expect(sapAiCore.getProviderForModel("gemini-pro")).toBe("Google") + expect(sapAiCore.getProviderForModel("non-existent-model")).toBeUndefined() + + expect(axiosPostSpy).toHaveBeenCalledOnce() + expect(axiosGetSpy).toHaveBeenCalledTimes(2) + }) + }) + + describe("getProviderForModel", () => { + it("returns undefined for undefined model ID", () => { + const provider = sapAiCore.getProviderForModel(undefined) + expect(provider).toBeUndefined() + }) + + it("returns undefined for non-existent model ID", () => { + const provider = sapAiCore.getProviderForModel("non-existent-model") + expect(provider).toBeUndefined() + }) + }) + + describe("Error handling - with mocks", () => { + beforeEach(() => { + // Set nock to wild mode to disable interception + nockBack.setMode("wild") + vi.resetAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + // Reset nock back to lockdown mode + nockBack.setMode("lockdown") + }) + + it("handles authentication errors gracefully for models", async () => { + const axiosPostSpy = vi.spyOn(axios, "post").mockRejectedValueOnce(new Error("Authentication failed")) + + await expect(sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group")).rejects.toThrow( + "Failed to authenticate with SAP AI Core", + ) + + expect(axiosPostSpy).toHaveBeenCalledOnce() + }) + + it("handles API errors gracefully for models", async () => { + // Mock successful authentication + const axiosPostSpy = vi.spyOn(axios, "post").mockResolvedValueOnce({ + data: { access_token: "test-token", expires_in: 3600 }, + status: 200, + statusText: "OK", + headers: {}, + config: {}, + } as AxiosResponse) + + // Mock failed models API call - need to mock both calls since fetchModels makes parallel requests + const axiosGetSpy = vi.spyOn(axios, "get").mockRejectedValueOnce(new Error("API error")) + + await expect(sapAiCore.getSapAiCoreModels(mockServiceKey, "test-resource-group")).rejects.toThrow( + "Failed to fetch SAP AI Core models", + ) + + expect(axiosPostSpy).toHaveBeenCalledOnce() + expect(axiosGetSpy).toHaveBeenCalled() + }) + + it("handles authentication errors gracefully for deployments", async () => { + const axiosPostSpy = vi.spyOn(axios, "post").mockRejectedValueOnce(new Error("Authentication failed")) + + await expect(sapAiCore.getSapAiCoreDeployments(mockServiceKey, "test-resource-group")).rejects.toThrow( + "Failed to authenticate with SAP AI Core", + ) + + expect(axiosPostSpy).toHaveBeenCalledOnce() + }) + + it("handles API errors gracefully for deployments", async () => { + // Mock successful authentication + const axiosPostSpy = vi.spyOn(axios, "post").mockResolvedValueOnce({ + data: { access_token: "test-token", expires_in: 3600 }, + status: 200, + statusText: "OK", + headers: {}, + config: {}, + } as AxiosResponse) + + // Mock failed deployments API call + const axiosGetSpy = vi.spyOn(axios, "get").mockRejectedValueOnce(new Error("API error")) + + await expect(sapAiCore.getSapAiCoreDeployments(mockServiceKey, "test-resource-group")).rejects.toThrow( + "Failed to fetch SAP AI Core deployments", + ) + + expect(axiosPostSpy).toHaveBeenCalledOnce() + expect(axiosGetSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index e4ff335e5d0..8ccb3b95459 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -31,6 +31,7 @@ import { getGeminiModels } from "./gemini" import { getDeepInfraModels } from "./deepinfra" import { getHuggingFaceModels } from "./huggingface" +import { getSapAiCoreModels } from "./sap-ai-core" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -133,6 +134,13 @@ export const getModels = async (options: GetModelsOptions): Promise case "huggingface": models = await getHuggingFaceModels() break + case "sap-ai-core": + models = await getSapAiCoreModels( + options.sapAiCoreServiceKey, + options.sapAiCoreResourceGroup, + options.sapAiCoreUseOrchestration, + ) + break // kilocode_change start case "ovhcloud": models = await getOvhCloudAiEndpointsModels() diff --git a/src/api/providers/fetchers/sap-ai-core.ts b/src/api/providers/fetchers/sap-ai-core.ts new file mode 100644 index 00000000000..e4fd8ee2132 --- /dev/null +++ b/src/api/providers/fetchers/sap-ai-core.ts @@ -0,0 +1,383 @@ +import axios from "axios" +import { ModelInfo } from "@roo-code/types" +import type { ModelRecord } from "../../../shared/api" + +// Cache for mapping model IDs to providers +const modelProviderCache: Record = {} + +interface Deployment { + id: string + name: string + model: string + targetStatus: string +} + +interface Token { + access_token: string + expires_in: number + scope: string + jti: string + token_type: string + expires_at: number +} + +export interface SapAiCoreModel { + model: string + provider: Provider + allowedScenarios: Scenario[] + modelInfo: ModelInfo + deployments?: Deployment[] +} + +const PROVIDERS = ["Amazon", "Anthropic", "Google", "OpenAI", "Mistral AI"] as const +export type Provider = (typeof PROVIDERS)[number] +export type Scenario = "Foundation" | "Orchestration" +export type DeploymentRecord = Record + +interface SapAiCoreConfig { + serviceKey?: string + resourceGroup?: string +} + +interface SapAiCoreServiceKeyConfig { + clientid: string + clientsecret: string + url: string + identityzone?: string + identityzoneid?: string + appname?: string + "credential-type"?: string + serviceurls: { + AI_API_URL: string + } +} + +class SapAiCoreFetcher { + private serviceKeyConfig: SapAiCoreServiceKeyConfig + + constructor(private config: SapAiCoreConfig) { + this.config.resourceGroup = config.resourceGroup || "default" + + if (!this.config.serviceKey) { + throw new Error("SAP AI Core service key is required") + } + + try { + this.serviceKeyConfig = JSON.parse(this.config.serviceKey) as SapAiCoreServiceKeyConfig + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to parse SAP AI Core service key: ${error.message}`) + } + throw new Error("Failed to parse SAP AI Core service key") + } + } + + async getDeployments(): Promise { + return this.fetchDeployments() + } + + async getModels(): Promise> { + return this.fetchModels() + } + + private async getToken(): Promise { + const token = await this.authenticate() + return token.access_token + } + + private async authenticate(): Promise { + const payload = { + grant_type: "client_credentials", + client_id: this.serviceKeyConfig.clientid, + client_secret: this.serviceKeyConfig.clientsecret, + } + + const tokenUrl = this.serviceKeyConfig.url.replace(/\/+$/, "") + "/oauth/token" + + try { + const response = await axios.post(tokenUrl, payload, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }) + + const token = response.data as Token + token.expires_at = Date.now() + token.expires_in * 1000 + return token + } catch (error) { + console.error("Authentication failed:", error) + throw new Error("Failed to authenticate with SAP AI Core") + } + } + + private async getHeaders(): Promise> { + const token = await this.getToken() + return { + Authorization: `Bearer ${token}`, + "AI-Resource-Group": this.config.resourceGroup!, + "Content-Type": "application/json", + "AI-Client-Type": "Cline", + } + } + + async fetchModels(): Promise> { + const headers = await this.getHeaders() + const baseUrl = this.serviceKeyConfig.serviceurls.AI_API_URL + const url = `${baseUrl}/v2/lm/scenarios/foundation-models/models` + const deploymentsUrl = `${baseUrl}/v2/lm/deployments?$top=10000&$skip=0` + + try { + const [modelsResponse, deploymentsResponse] = await Promise.all([ + axios.get(url, { headers }), + axios.get(deploymentsUrl, { headers }), + ]) + + const allModels = modelsResponse.data.resources + const allDeployments = deploymentsResponse.data.resources + + // Transform deployments + const deployments: DeploymentRecord = {} + allDeployments.forEach((deployment: any) => { + const transformedDeployment = this.transformDeployment(deployment) + if (transformedDeployment) { + deployments[transformedDeployment.id] = transformedDeployment + } + }) + + // Filter raw models first, then transform + const filteredRawModels = this.filterRawModels(allModels) + return this.transformModels(filteredRawModels, Object.values(deployments)) + } catch (error) { + console.error("Error fetching SAP AI Core models:", error) + throw new Error("Failed to fetch SAP AI Core models") + } + } + + private filterRawModels(allModels: any[]): any[] { + return allModels.filter((model: any) => { + // Filter: Supported providers only + if (!this.isSupportedProvider(model.provider)) return false + + // Find the latest version and apply streaming/text-generation filters + const latestVersion = model.versions?.find((version: any) => version.isLatest === true) + if (!latestVersion) return false + + // Filter: Must support streaming + if (!latestVersion.streamingSupported) return false + + // Filter: Must support text-generation + if (!latestVersion.capabilities?.includes("text-generation")) return false + + // Filter: Context window must be at least 32000 + return !(!latestVersion.contextLength || latestVersion.contextLength < 32000) + }) + } + + private async fetchDeployments(): Promise { + const headers = await this.getHeaders() + const baseUrl = this.serviceKeyConfig.serviceurls.AI_API_URL + const url = `${baseUrl}/v2/lm/deployments?$top=10000&$skip=0` + + try { + const response = await axios.get(url, { headers }) + const allDeployments = response.data.resources + + const deployments: DeploymentRecord = {} + + allDeployments.forEach((deployment: any) => { + const transformedDeployment = this.transformDeployment(deployment) + if (transformedDeployment) { + deployments[transformedDeployment.id] = transformedDeployment + } + }) + + return deployments + } catch (error) { + console.error("Error fetching SAP AI Core deployments:", error) + throw new Error("Failed to fetch SAP AI Core deployments") + } + } + + private transformDeployment(deployment: any): Deployment | null { + const model = deployment.details?.resources?.backend_details?.model + if (!model?.name || !model?.version) { + return null + } + + return { + id: deployment.id, + name: `${model.name}:${model.version}`, + model: model.name, + targetStatus: deployment.targetStatus, + } + } + + private transformModels(allModels: any[], deployments: Deployment[]): Record { + const result: Record = {} + + allModels.forEach((model: any) => { + const transformedModel = this.transformModel(model, deployments) + if (transformedModel) { + result[model.model] = transformedModel + } + }) + + return result + } + + private transformModel(model: any, deployments: Deployment[]): SapAiCoreModel | null { + const latestVersion = model.versions?.find((version: any) => version.isLatest === true) + if (!latestVersion) { + return null + } + + const allowedScenarios = this.mapScenarios(model.allowedScenarios) + const modelInfo = this.createModelInfo(latestVersion, model) + const modelDeployments = this.findModelDeployments(model.model, deployments) + + return { + model: model.model, + provider: model.provider as Provider, + allowedScenarios, + modelInfo, + deployments: modelDeployments, + } + } + + private mapScenarios(scenarios: any[]): Scenario[] { + return scenarios + .map((scenario: any) => { + if (scenario.scenarioId === "foundation-models") return "Foundation" + if (scenario.scenarioId === "orchestration") return "Orchestration" + return null + }) + .filter((scenario: Scenario | null): scenario is Scenario => scenario !== null) + } + + private createModelInfo(latestVersion: any, model: any): ModelInfo { + const inputCost = latestVersion.cost?.find((c: any) => c.inputCost)?.inputCost + const outputCost = latestVersion.cost?.find((c: any) => c.outputCost)?.outputCost + + return { + contextWindow: latestVersion.contextLength, + supportsImages: + latestVersion.inputTypes?.includes("image") || + latestVersion.capabilities?.includes("image-recognition"), + supportsPromptCache: false, + inputPrice: inputCost ? parseFloat(inputCost) : undefined, + outputPrice: outputCost ? parseFloat(outputCost) : undefined, + description: model.description, + displayName: model.displayName, + preferredIndex: undefined, + } + } + + private findModelDeployments(modelName: string, deployments: Deployment[]): Deployment[] { + const modelBaseName = modelName.split(":")[0].toLowerCase() + return deployments.filter((deployment) => { + const deploymentBaseName = deployment.name.split(":")[0].toLowerCase() + return deploymentBaseName === modelBaseName + }) + } + + private isSupportedProvider(value: string): value is Provider { + return PROVIDERS.includes(value as Provider) + } +} + +export async function getSapAiCoreModels( + sapAiCoreServiceKey?: string, + sapAiCoreResourceGroup?: string, + sapAiCoreUseOrchestration?: boolean, +): Promise { + const client = new SapAiCoreFetcher({ + serviceKey: sapAiCoreServiceKey, + resourceGroup: sapAiCoreResourceGroup, + }) + + const allModels = await client.getModels() + const filteredModels = filterModelsByScenario(allModels, sapAiCoreUseOrchestration) + + // Populate the model-to-provider cache + populateModelProviderCache(filteredModels) + + const modelRecord: ModelRecord = {} + + Object.entries(filteredModels).forEach(([modelId, sapAiCoreModel]) => { + modelRecord[modelId] = sapAiCoreModel.modelInfo + }) + + return modelRecord +} + +export async function getSapAiCoreDeployments( + sapAiCoreServiceKey?: string, + sapAiCoreResourceGroup: string = "default", +): Promise { + const client = new SapAiCoreFetcher({ + serviceKey: sapAiCoreServiceKey, + resourceGroup: sapAiCoreResourceGroup, + }) + + return client.getDeployments() +} + +// Helper function to filter models by scenario and context window +function filterModelsByScenario( + allModels: Record, + sapAiCoreUseOrchestration?: boolean, +): Record { + const filteredModels: Record = {} + + // Filter models based on scenario (context window filtering already done in fetchModels) + Object.entries(allModels).forEach(([modelId, sapAiCoreModel]) => { + // If no orchestration preference specified, include all models + if (sapAiCoreUseOrchestration === undefined) { + filteredModels[modelId] = sapAiCoreModel + return + } + + const hasOrchestration = sapAiCoreModel.allowedScenarios.includes("Orchestration") + const hasFoundation = sapAiCoreModel.allowedScenarios.includes("Foundation") + + // If useOrchestration is true, only include models that support orchestration + // If useOrchestration is false, only include foundation models that are also supported + if (sapAiCoreUseOrchestration) { + // For orchestration, include all models that support orchestration + if (hasOrchestration) { + filteredModels[modelId] = sapAiCoreModel + } + } else { + // For foundation models, check both foundation support AND if the model is in our supported list + if (hasFoundation && isProviderSupportedByFoundation(sapAiCoreModel)) { + filteredModels[modelId] = sapAiCoreModel + } + } + }) + + return filteredModels +} + +// SAP AI Core's official SDK currently only supports OpenAI models. +function isProviderSupportedByFoundation(sapAiCoreModel: SapAiCoreModel): boolean { + return sapAiCoreModel.provider === "OpenAI" +} + +/** + * Populate the model-to-provider cache with fetched models (only by modelId) + */ +function populateModelProviderCache(models: Record): void { + // Clear cache by creating new empty object + Object.keys(modelProviderCache).forEach((key) => delete modelProviderCache[key]) + + Object.entries(models).forEach(([modelId, sapAiCoreModel]) => { + modelProviderCache[modelId] = sapAiCoreModel.provider + }) +} + +/** + * Get the provider for a given model ID + * @param modelId - The model ID to look up + * @returns The provider for the model, or undefined if not found + */ +export function getProviderForModel(modelId?: string): Provider | undefined { + return modelId ? modelProviderCache[modelId] : undefined +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 1e87b24e4ea..5fb007afa15 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -40,3 +40,4 @@ export { RooHandler } from "./roo" export { FeatherlessHandler } from "./featherless" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" export { DeepInfraHandler } from "./deepinfra" +export { SapAiCoreHandler } from "./sap-ai-core" diff --git a/src/api/providers/sap-ai-core.ts b/src/api/providers/sap-ai-core.ts new file mode 100644 index 00000000000..d02aaea7a36 --- /dev/null +++ b/src/api/providers/sap-ai-core.ts @@ -0,0 +1,331 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import type { ApiHandlerOptions, ModelRecord } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { BaseProvider } from "./base-provider" +import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" + +// SAP AI SDK imports +import { ChatMessage, OrchestrationClient, OrchestrationModuleConfig } from "@sap-ai-sdk/orchestration" +import { AzureOpenAiChatClient, AzureOpenAiChatCompletionRequestMessage } from "@sap-ai-sdk/foundation-models" +import { openAiModelInfoSaneDefaults } from "@roo-code/types" +import { getProviderForModel, getSapAiCoreModels, Provider } from "./fetchers/sap-ai-core" + +/** + * SAP AI Core provider supporting both Orchestration and Foundation Models modes + */ +export class SapAiCoreHandler extends BaseProvider implements SingleCompletionHandler { + protected options: ApiHandlerOptions + private readonly providerName = "SAP AI Core" + private isAiCoreEnvSetup = false + private backend: SapAiCoreBackend + + private modelCache: ModelRecord | null = null + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + this.backend = this.createBackend() + + // Fetch models asynchronously + this.fetchModels() + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + _: ApiHandlerCreateMessageMetadata, + ): ApiStream { + this.ensureAiCoreEnvSetup() + yield* this.backend.createMessage(systemPrompt, messages) + } + + async completePrompt(prompt: string): Promise { + this.ensureAiCoreEnvSetup() + + try { + return await this.backend.completePrompt(prompt) + } catch (error) { + if (error instanceof Error) { + throw new Error(`${this.providerName} completion error: ${error.message}`) + } + throw error + } + } + + private async fetchModels() { + try { + this.modelCache = await getSapAiCoreModels( + this.options.sapAiCoreServiceKey, + this.options.sapAiCoreResourceGroup, + this.options.sapAiCoreUseOrchestration, + ) + } catch (error) { + console.error("Failed to fetch SAP AI Core models:", error) + } + } + + override getModel() { + const modelId = this.options.sapAiCoreModelId || "gpt-4o" + + // Try to get model info from cache + const modelInfo = this.modelCache?.[modelId] + + if (modelInfo) { + return { + id: modelId, + info: modelInfo, + } + } + + // Fallback to default values if model not found in cache + return { + id: modelId, + info: openAiModelInfoSaneDefaults, + } + } + + private createBackend(): SapAiCoreBackend { + const useOrchestration = this.options.sapAiCoreUseOrchestration ?? false + + if (useOrchestration) { + return new OrchestrationBackend(this.options) + } else { + return new FoundationBackend(this.options) + } + } + + private ensureAiCoreEnvSetup() { + if (this.isAiCoreEnvSetup) { + return + } + + process.env["AICORE_SERVICE_KEY"] = this.options.sapAiCoreServiceKey + + this.isAiCoreEnvSetup = true + } +} + +// Base class for all SAP AI Core backends +abstract class SapAiCoreBackend { + protected options: ApiHandlerOptions + + constructor(options: ApiHandlerOptions) { + this.options = options + } + + abstract createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream + + abstract completePrompt(prompt: string): Promise + + protected processUsageMetrics(tokenUsage: any): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: tokenUsage?.prompt_tokens || 0, + outputTokens: tokenUsage?.completion_tokens || 0, + cacheWriteTokens: tokenUsage?.cache_creation_input_tokens || undefined, + cacheReadTokens: tokenUsage?.cache_read_input_tokens || undefined, + } + } + + protected getModelParams() { + const params: any = {} + + if (this.options.modelTemperature !== undefined) { + params.temperature = this.options.modelTemperature + } + + if (this.options.modelMaxTokens) { + params.max_tokens = this.options.modelMaxTokens + } + + return Object.keys(params).length > 0 ? params : undefined + } +} + +// Orchestration backend implementation +class OrchestrationBackend extends SapAiCoreBackend { + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + try { + const modelId = this.options.sapAiCoreModelId! + const orchestrationClient = this.createOrchestrationClient(modelId, systemPrompt) + const chatMessages = this.convertToOrchestrationMessages(messages) + + const response = await orchestrationClient.stream({ + messages: chatMessages, + }) + + for await (const chunk of response.stream.toContentStream()) { + yield { type: "text", text: chunk } + } + + const tokenUsage = response.getTokenUsage() + if (tokenUsage) { + yield this.processUsageMetrics(tokenUsage) + } + } catch (error) { + console.error("Error in SAP orchestration mode:", error) + throw error + } + } + + async completePrompt(prompt: string): Promise { + const modelId = this.options.sapAiCoreModelId! + const orchestrationClient = this.createOrchestrationClient(modelId) + const response = await orchestrationClient.chatCompletion({ + messages: [{ role: "user", content: prompt }], + }) + return response.getContent() || "" + } + + private createOrchestrationClient(modelName: string, systemPrompt?: string): OrchestrationClient { + const config: OrchestrationModuleConfig = { + promptTemplating: { + model: { + name: modelName, + ...(this.getModelParams() && { params: this.getModelParams() }), + }, + ...(systemPrompt && { + prompt: { + template: [ + { + role: "system", + content: systemPrompt, + }, + ], + }, + }), + }, + } + + const resourceGroup = this.options.sapAiCoreResourceGroup ?? "default" + const resourceGroupConfig: any = { resourceGroup: resourceGroup } + + return new OrchestrationClient(config, resourceGroupConfig) + } + + private convertToOrchestrationMessages(messages: Anthropic.Messages.MessageParam[]): ChatMessage[] { + return convertToOpenAiMessages(messages) as ChatMessage[] + } +} + +// Foundation backend that routes to appropriate provider +class FoundationBackend extends SapAiCoreBackend { + private providerBackend: SapAiCoreBackend + + constructor(options: ApiHandlerOptions) { + super(options) + this.providerBackend = this.createProviderBackend() + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + yield* this.providerBackend.createMessage(systemPrompt, messages) + } + + async completePrompt(prompt: string): Promise { + return this.providerBackend.completePrompt(prompt) + } + + private createProviderBackend(): SapAiCoreBackend { + const provider = this.getModelProvider() + + switch (provider) { + case "OpenAI": + return new OpenAIFoundationBackend(this.options) + case "Anthropic": + throw new Error("Anthropic foundation models not yet supported") + case "Google": + throw new Error("Google foundation models not yet supported") + case "Amazon": + throw new Error("Amazon foundation models not yet supported") + case "Mistral AI": + throw new Error("Mistral foundation models not yet supported") + default: + throw new Error(`Unsupported provider: ${provider}`) + } + } + + private getModelProvider(): Provider | undefined { + return getProviderForModel(this.options.sapAiCoreModelId) + } +} + +// OpenAI Foundation backend +class OpenAIFoundationBackend extends SapAiCoreBackend { + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const foundationClient = this.createOpenAIClient() + const chatMessages = this.convertToFoundationMessages(systemPrompt, messages) + + const response = await foundationClient.stream({ + messages: chatMessages, + }) + + for await (const chunk of response.stream.toContentStream()) { + yield { type: "text", text: chunk } + } + + const tokenUsage = response.getTokenUsage() + if (tokenUsage) { + yield this.processUsageMetrics(tokenUsage) + } + } + + async completePrompt(prompt: string): Promise { + const foundationClient = this.createOpenAIClient() + const response = await foundationClient.run({ + messages: [{ role: "user", content: prompt }], + }) + return response.getContent() || "" + } + + private createOpenAIClient(): AzureOpenAiChatClient { + const deploymentId = this.options.sapAiCoreDeploymentId ?? "" + const resourceGroup = this.options.sapAiCoreResourceGroup ?? "default" + return new AzureOpenAiChatClient({ + deploymentId: deploymentId, + resourceGroup: resourceGroup, + }) + } + + private convertToFoundationMessages( + systemPrompt: string, + messageParams: Anthropic.Messages.MessageParam[], + ): AzureOpenAiChatCompletionRequestMessage[] { + const convertedMessages = messageParams.map((param) => { + let content: string + + if (typeof param.content === "string") { + content = param.content + } else if (Array.isArray(param.content)) { + content = param.content + .map((block) => { + if (block.type === "text") { + return block.text + } else if (block.type === "image") { + return "[Image content]" + } + return "" + }) + .join("\n") + } else { + content = "" + } + + let role: "system" | "user" | "assistant" + switch (param.role) { + case "user": + role = "user" + break + case "assistant": + role = "assistant" + break + default: + role = "system" + } + + return { role, content } + }) + + return [{ role: "system" as const, content: systemPrompt }, ...convertedMessages] + } +} diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 66b7673bbf7..e098a03746c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -85,6 +85,7 @@ import { UsageTracker } from "../../utils/usage-tracker" import { seeNewChanges } from "../checkpoints/kilocode/seeNewChanges" // kilocode_change import { getTaskHistory } from "../../shared/kilocode/getTaskHistory" // kilocode_change import { fetchAndRefreshOrganizationModesOnStartup, refreshOrganizationModes } from "./kiloWebviewMessgeHandlerHelpers" +import { getSapAiCoreDeployments } from "../../api/providers/fetchers/sap-ai-core" // kilocode_change export const webviewMessageHandler = async ( provider: ClineProvider, @@ -814,6 +815,7 @@ export const webviewMessageHandler = async ( ollama: {}, lmstudio: {}, ovhcloud: {}, // kilocode_change + "sap-ai-core": {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -1035,6 +1037,49 @@ export const webviewMessageHandler = async ( provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] }) } break + case "requestSapAiCoreModels": { + // Specific handler for SAP AI Core models only. + if (message?.values?.sapAiCoreServiceKey) { + try { + // Flush cache first to ensure fresh models. + await flushModels("sap-ai-core") + + const sapAiCoreModels = await getModels({ + provider: "sap-ai-core", + sapAiCoreServiceKey: message?.values?.sapAiCoreServiceKey, + sapAiCoreResourceGroup: message?.values?.sapAiCoreResourceGroup, + sapAiCoreUseOrchestration: message?.values?.sapAiCoreUseOrchestration, + }) + + if (Object.keys(sapAiCoreModels).length > 0) { + provider.postMessageToWebview({ type: "sapAiCoreModels", sapAiCoreModels: sapAiCoreModels }) + } + } catch (error) { + console.error("SAP AI Core models fetch failed:", error) + } + } + break + } + case "requestSapAiCoreDeployments": { + if (message?.values?.sapAiCoreServiceKey) { + try { + const sapAiCoreDeployments = await getSapAiCoreDeployments( + message?.values?.sapAiCoreServiceKey, + message?.values?.sapAiCoreResourceGroup, + ) + + if (Object.keys(sapAiCoreDeployments).length > 0) { + provider.postMessageToWebview({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: sapAiCoreDeployments, + }) + } + } catch (error) { + console.error("SAP AI Core deployments fetch failed:", error) + } + } + break + } case "openImage": openImage(message.text!, { values: message.values }) break diff --git a/src/package.json b/src/package.json index 5babd658cb3..a11a9fd923d 100644 --- a/src/package.json +++ b/src/package.json @@ -642,6 +642,8 @@ "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", + "@sap-ai-sdk/foundation-models": "^2.0.0", + "@sap-ai-sdk/orchestration": "^2.0.0", "@vscode/codicons": "^0.0.36", "async-mutex": "^0.5.0", "axios": "^1.12.0", diff --git a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts index aadffe06516..dd3aaefa009 100644 --- a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts +++ b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/arrowFunctions.ts @@ -1,29 +1,29 @@ // @ts-nocheck const getAddress = (person: Person): Address => { - // TODO -}; + // TODO +} const logPerson = (person: Person) => { - // TODO -}; + // TODO +} const getHardcodedAddress = (): Address => { - // TODO -}; + // TODO +} const getAddresses = (people: Person[]): Address[] => { - // TODO -}; + // TODO +} const logPersonWithAddres = (person: Person
): Person
=> { - // TODO -}; + // TODO +} const logPersonOrAddress = (person: Person | Address): Person | Address => { - // TODO -}; + // TODO +} const logPersonAndAddress = (person: Person, address: Address) => { - // TODO -}; + // TODO +} diff --git a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts index 08f7506a70e..04cc29ae7fc 100644 --- a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts +++ b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/classMethods.ts @@ -1,35 +1,35 @@ // @ts-nocheck class Group { - getPersonAddress(person: Person): Address { - // TODO - } + getPersonAddress(person: Person): Address { + // TODO + } - getHardcodedAddress(): Address { - // TODO - } + getHardcodedAddress(): Address { + // TODO + } - addPerson(person: Person) { - // TODO - } + addPerson(person: Person) { + // TODO + } - addPeople(people: Person[]) { - // TODO - } + addPeople(people: Person[]) { + // TODO + } - getAddresses(people: Person[]): Address[] { - // TODO - } + getAddresses(people: Person[]): Address[] { + // TODO + } - logPersonWithAddress(person: Person
): Person
{ - // TODO - } + logPersonWithAddress(person: Person
): Person
{ + // TODO + } - logPersonOrAddress(person: Person | Address): Person | Address { - // TODO - } + logPersonOrAddress(person: Person | Address): Person | Address { + // TODO + } - logPersonAndAddress(person: Person, address: Address) { - // TODO - } + logPersonAndAddress(person: Person, address: Address) { + // TODO + } } diff --git a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts index bb9d39c85d9..ab3c2ebcb3a 100644 --- a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts +++ b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/functions.ts @@ -1,33 +1,33 @@ // @ts-nocheck function getAddress(person: Person): Address { - // TODO + // TODO } function getFirstAddress(people: Person[]): Address { - // TODO + // TODO } function logPerson(person: Person) { - // TODO + // TODO } function getHardcodedAddress(): Address { - // TODO + // TODO } function getAddresses(people: Person[]): Address[] { - // TODO + // TODO } function logPersonWithAddress(person: Person
): Person
{ - // TODO + // TODO } function logPersonOrAddress(person: Person | Address): Person | Address { - // TODO + // TODO } function logPersonAndAddress(person: Person, address: Address) { - // TODO + // TODO } diff --git a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts index 79811b47873..4ad998ed9fd 100644 --- a/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts +++ b/src/services/continuedev/core/autocomplete/context/root-path-context/__fixtures__/files/typescript/generators.ts @@ -1,33 +1,33 @@ // @ts-nocheck function* getAddress(person: Person): Address { - // TODO + // TODO } function* getFirstAddress(people: Person[]): Address { - // TODO + // TODO } function* logPerson(person: Person) { - // TODO + // TODO } function* getHardcodedAddress(): Address { - // TODO + // TODO } function* getAddresses(people: Person[]): Address[] { - // TODO + // TODO } function* logPersonWithAddress(person: Person
): Person
{ - // TODO + // TODO } function* logPersonOrAddress(person: Person | Address): Person | Address { - // TODO + // TODO } function* logPersonAndAddress(person: Person, address: Address) { - // TODO + // TODO } diff --git a/src/services/continuedev/core/nextEdit/README.md b/src/services/continuedev/core/nextEdit/README.md index 2c78f484f35..feca83d76db 100644 --- a/src/services/continuedev/core/nextEdit/README.md +++ b/src/services/continuedev/core/nextEdit/README.md @@ -9,21 +9,21 @@ - Users can decide to switch between autocomplete and next edit. - Next edit triggers at the same time autocomplete is triggered, via vscode's inline completion provider. - The following happens after the trigger: - - User's current cursor position is captured. - - We define an editable range, ±5 lines from the current cursor position. - - User's most recent edit is captured as a unified diff. (this is currently buggy) - - This is sent to the model, which returns a new editable range with next edit predictions. - - We display this new editable range in a SVG decoration. - - User can either tab to accept or esc to reject. - - On accept, the old editable range will be replaced by the new editable region. The cursor will be moved to the last line containing some change. - - On reject, nothing happens. + - User's current cursor position is captured. + - We define an editable range, ±5 lines from the current cursor position. + - User's most recent edit is captured as a unified diff. (this is currently buggy) + - This is sent to the model, which returns a new editable range with next edit predictions. + - We display this new editable range in a SVG decoration. + - User can either tab to accept or esc to reject. + - On accept, the old editable range will be replaced by the new editable region. The cursor will be moved to the last line containing some change. + - On reject, nothing happens. ## What needs to be worked on? - User edit captures. - Find a better way to trigger next edit (this links back to the diff capture problem). - - We can see that next edit triggers as soon as the user accepts a change. This is because autocomplete runs the same way. - - I think autocomplete has some filter logic that doesn't display the ghost text under some conditions, which I am guessing are the following: - - The model does not have any more completions to create. - - The prediction at the cursor location has been cached. + - We can see that next edit triggers as soon as the user accepts a change. This is because autocomplete runs the same way. + - I think autocomplete has some filter logic that doesn't display the ghost text under some conditions, which I am guessing are the following: + - The model does not have any more completions to create. + - The prediction at the cursor location has been cached. - JetBrains integration. diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index fc39807e4aa..33584aff8a2 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -31,6 +31,7 @@ import { } from "./WebviewMessage" import { ClineRulesToggles } from "./cline-rules" import { KiloCodeWrapperProperties } from "./kilocode/wrapper" +import { DeploymentRecord } from "../api/providers/fetchers/sap-ai-core" // kilocode_change end // Command interface for frontend/backend communication @@ -92,6 +93,8 @@ export interface ExtensionMessage { | "lmStudioModels" | "vsCodeLmModels" | "huggingFaceModels" + | "sapAiCoreModels" + | "sapAiCoreDeployments" | "vsCodeLmApiAvailable" | "updatePrompt" | "systemPrompt" @@ -208,6 +211,8 @@ export interface ExtensionMessage { } }> }> + sapAiCoreModels?: ModelRecord + sapAiCoreDeployments?: DeploymentRecord mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ProviderSettingsEntry[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index f04a7cf043e..78319e2bd20 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -95,6 +95,8 @@ export interface WebviewMessage { | "requestLmStudioModels" | "requestVsCodeLmModels" | "requestHuggingFaceModels" + | "requestSapAiCoreModels" + | "requestSapAiCoreDeployments" | "openImage" | "saveImage" | "openFile" diff --git a/src/shared/api.ts b/src/shared/api.ts index 4b8b17d392a..010460fe99d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -169,6 +169,11 @@ const dynamicProviderExtras = { lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type ovhcloud: {} as { apiKey?: string }, // kilocode_change chutes: {} as { apiKey?: string }, // kilocode_change + "sap-ai-core": {} as { + sapAiCoreServiceKey?: string + sapAiCoreResourceGroup?: string + sapAiCoreUseOrchestration?: boolean + }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts index 68bb37c9771..b50822edc6f 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -32,6 +32,7 @@ describe("getModelsByProvider", () => { ovhcloud: { "test-model": testModel }, chutes: { "test-model": testModel }, // kilocode_change end + "sap-ai-core": { "test-model": testModel }, } const exceptions = [ diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index a31d3c9663f..28f9ed6718d 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -107,6 +107,7 @@ import { VercelAiGateway, DeepInfra, OvhCloudAiEndpoints, // kilocode_change + SapAiCore, } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -793,6 +794,10 @@ const ApiOptions = ({ )} + {selectedProvider === "sap-ai-core" && ( + + )} + {selectedProviderModels.length > 0 && ( <>
diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 961a3cd4743..91e8cff99e0 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -97,6 +97,7 @@ export const PROVIDERS = [ // { value: "roo", label: "Roo Code Cloud" }, // kilocode_change end { value: "vercel-ai-gateway", label: "Vercel AI Gateway" }, + { value: "sap-ai-core", label: "SAP AI Core" }, ].sort((a, b) => a.label.localeCompare(b.label)) PROVIDERS.unshift({ value: "kilocode", label: "Kilo Code" }) // kilocode_change diff --git a/webview-ui/src/components/settings/providers/SapAiCore.tsx b/webview-ui/src/components/settings/providers/SapAiCore.tsx new file mode 100644 index 00000000000..b3ebe29c6a7 --- /dev/null +++ b/webview-ui/src/components/settings/providers/SapAiCore.tsx @@ -0,0 +1,335 @@ +import { useCallback, useEffect, useMemo, useState } from "react" +import { useEvent } from "react-use" +import { vscode } from "@src/utils/vscode" + +import { ExtensionMessage } from "@roo/ExtensionMessage" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { Checkbox, Input, SearchableSelect, type SearchableSelectOption } from "@src/components/ui" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" + +import { inputEventTransform } from "../transforms" +import { DeploymentRecord } from "../../../../../src/api/providers/fetchers/sap-ai-core" +import { ModelInfoView } from "@/components/settings/ModelInfoView" +import { ModelRecord } from "@roo/api" + +type SapAiCoreProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: ( + field: keyof ProviderSettings, + value: ProviderSettings[keyof ProviderSettings], + isUserAction?: boolean, + ) => void +} + +// Fields that trigger model/deployment refetch when changed +const REFETCH_FIELDS = ["sapAiCoreServiceKey", "sapAiCoreResourceGroup", "sapAiCoreUseOrchestration"] as const + +const SapAiCore = ({ apiConfiguration, setApiConfigurationField }: SapAiCoreProps) => { + const { t } = useAppTranslation() + const [models, setModels] = useState({}) + const [deployments, setDeployments] = useState({}) + const [loading, setLoading] = useState(false) + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) + + // Extracted common auth values to avoid repetition + const sapAiCoreConfiguration = useMemo( + () => ({ + sapAiCoreServiceKey: apiConfiguration.sapAiCoreServiceKey || "", + sapAiCoreResourceGroup: apiConfiguration.sapAiCoreResourceGroup || "", + sapAiCoreUseOrchestration: apiConfiguration.sapAiCoreUseOrchestration || false, + }), + [apiConfiguration], + ) + + const fetchModels = useCallback(() => { + setLoading(true) + vscode.postMessage({ + type: "requestSapAiCoreModels", + values: sapAiCoreConfiguration, + }) + }, [sapAiCoreConfiguration]) + + const fetchDeployments = useCallback(() => { + setLoading(true) + vscode.postMessage({ + type: "requestSapAiCoreDeployments", + values: sapAiCoreConfiguration, + }) + }, [sapAiCoreConfiguration]) + + const clearModelSelection = useCallback(() => { + // Clear model-related fields when toggling orchestration + const fieldsToReset = ["sapAiCoreModelId", "sapAiCoreCustomModelInfo", "sapAiCoreDeploymentId"] + fieldsToReset.forEach((field) => setApiConfigurationField(field as keyof ProviderSettings, undefined)) + }, [setApiConfigurationField]) + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + if (REFETCH_FIELDS.includes(field as any)) { + clearModelSelection() + fetchModels() + fetchDeployments() + } + }, + [setApiConfigurationField, fetchModels, fetchDeployments, clearModelSelection], + ) + + useEffect(() => { + fetchModels() + fetchDeployments() + }, [fetchModels, fetchDeployments]) + + const onMessage = useCallback((event: MessageEvent) => { + const message: ExtensionMessage = event.data + + switch (message.type) { + case "sapAiCoreModels": + setModels(message.sapAiCoreModels || {}) + setLoading(false) + break + case "sapAiCoreDeployments": + setDeployments(message.sapAiCoreDeployments || {}) + setLoading(false) + break + } + }, []) + + useEvent("message", onMessage) + + // Simplified handlers + const handleModelSelect = (modelId: string) => { + setApiConfigurationField("sapAiCoreModelId", modelId) + setApiConfigurationField("sapAiCoreCustomModelInfo", models[modelId]) + setApiConfigurationField("sapAiCoreDeploymentId", undefined) + } + + const handleDeploymentSelect = (deploymentId: string) => { + setApiConfigurationField("sapAiCoreDeploymentId", deploymentId) + } + + const handleUseOrchestrationSelect = (useOrchestration: boolean) => { + clearModelSelection() + setApiConfigurationField("sapAiCoreUseOrchestration", useOrchestration) + } + + // Deployment filtering logic + const getAvailableDeployments = useCallback( + (modelId?: string) => { + return Object.values(deployments).filter((deployment) => deployment.model === modelId) + }, + [deployments], + ) + + const hasAvailableDeployments = useCallback( + (modelId?: string) => { + return getAvailableDeployments(modelId).length > 0 + }, + [getAvailableDeployments], + ) + + // Model options with sorting + const modelOptions = useMemo(() => { + return Object.keys(models) + .map( + (modelId): SearchableSelectOption => ({ + value: modelId, + label: modelId, + disabled: !apiConfiguration.sapAiCoreUseOrchestration && !hasAvailableDeployments(modelId), + }), + ) + .sort((a, b) => { + if (!apiConfiguration.sapAiCoreUseOrchestration && a.disabled !== b.disabled) { + return a.disabled ? 1 : -1 + } + return a.label.localeCompare(b.label) + }) + }, [models, apiConfiguration.sapAiCoreUseOrchestration, hasAvailableDeployments]) + + // Deployment options for selected model + const deploymentOptions = useMemo(() => { + if (!apiConfiguration.sapAiCoreModelId) return [] + + return getAvailableDeployments(apiConfiguration.sapAiCoreModelId) + .sort((a, b) => Number(b.targetStatus === "RUNNING") - Number(a.targetStatus === "RUNNING")) + .map( + (deployment): SearchableSelectOption => ({ + value: deployment.id, + label: + deployment.targetStatus === "RUNNING" + ? deployment.id + : `${deployment.id} (${deployment.targetStatus.toLowerCase()})`, + disabled: deployment.targetStatus !== "RUNNING", + }), + ) + }, [apiConfiguration.sapAiCoreModelId, getAvailableDeployments]) + + return ( +
+
+ + +
+ +
+ {t("settings:providers.sapAiCore.credentialsNote")}{" "} + + {t("settings:providers.sapAiCore.learnMore")} + +
+ +
+ + +
+ +
+ + +
+ +
+
{t("settings:providers.sapAiCore.orchestrationEnabledDesc")}
+
{t("settings:providers.sapAiCore.orchestrationDisabledDesc")}
+
+ + {!apiConfiguration.sapAiCoreUseOrchestration && ( +
+
+ + + {t("settings:providers.sapAiCore.supportedProviders")} + +
+ + {t("settings:providers.sapAiCore.supportedProvidersDesc1")} + +
    +
  • {t("settings:codeIndex.openaiProvider")}
  • +
+ + {t("settings:providers.sapAiCore.supportedProvidersDesc2")} + +
+ + {t("settings:providers.sapAiCore.supportedProvidersDesc3")} + +
+
+ )} + +
+ + + +
+ + {!apiConfiguration.sapAiCoreUseOrchestration && !apiConfiguration.sapAiCoreModelId && ( +
+
{t("settings:providers.sapAiCore.modelDeploymentDesc")}
+
+ )} + + {!apiConfiguration.sapAiCoreUseOrchestration && apiConfiguration.sapAiCoreModelId && ( +
+ + + +
+ )} + + {apiConfiguration.sapAiCoreModelId && ( + + )} + +
+ + {t("settings:providers.sapAiCore.getStarted")} + +
+
+ ) +} + +export default SapAiCore diff --git a/webview-ui/src/components/settings/providers/__tests__/SapAiCore.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/SapAiCore.spec.tsx new file mode 100644 index 00000000000..9ab882d00e5 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/SapAiCore.spec.tsx @@ -0,0 +1,1069 @@ +import React from "react" +import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" +import SapAiCore from "../SapAiCore" +import type { ProviderSettings } from "@roo-code/types" +import type { ModelRecord } from "@roo/api" +import { DeploymentRecord } from "../../../../../../src/api/providers/fetchers/sap-ai-core" + +// Mock vscode API +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock UI components +vi.mock("@src/components/ui", () => ({ + Checkbox: (props: any) => { + const { id, checked, onCheckedChange, children, ...restProps } = props + return ( + + ) + }, + Input: React.forwardRef((props: any, ref: any) => { + const { id, value, onInput, type, placeholder, ...restProps } = props + return ( + onInput?.(e)} + placeholder={placeholder} + data-testid={id} + {...restProps} + /> + ) + }), + SearchableSelect: (props: any) => { + const { value, onValueChange, options, placeholder, disabled, "data-testid": testId } = props + return ( + + ) + }, +})) + +// Mock VSCode components +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeLink: (props: any) => { + const { children, href, target } = props + return ( + + {children} + + ) + }, +})) + +// Mock ModelInfoView component +vi.mock("@/components/settings/ModelInfoView", () => ({ + ModelInfoView: (props: any) => { + const { isDescriptionExpanded, setIsDescriptionExpanded } = props + return ( +
+ +
+ ) + }, +})) + +// Mock transforms +vi.mock("../transforms", () => ({ + inputEventTransform: (event: any) => event.target?.value || event, +})) + +// Mock translation hook +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, options?: any) => { + if (options?.count !== undefined) { + return `${key} (${options.count})` + } + return key + }, + }), +})) + +// Mock react-use +let mockMessageHandler: ((event: MessageEvent) => void) | null = null +vi.mock("react-use", () => ({ + useEvent: vi.fn((eventType: string, handler: (event: MessageEvent) => void) => { + if (eventType === "message") { + mockMessageHandler = handler + } + }), +})) + +describe("SapAiCore Component", () => { + // Define all shared variables inside the describe block + const defaultApiConfiguration: Partial = { + sapAiCoreServiceKey: "", + sapAiCoreResourceGroup: "", + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "", + sapAiCoreDeploymentId: "", + apiProvider: "sap-ai-core", + } + + const mockSetApiConfigurationField = vi.fn() + let mockPostMessage: any + + beforeAll(async () => { + // Get the mocked postMessage function + const { vscode } = await import("@src/utils/vscode") + mockPostMessage = vscode.postMessage + }) + + // Helper function to simulate message events + const simulateMessageEvent = (data: any) => { + if (mockMessageHandler) { + const event = { data } as MessageEvent + mockMessageHandler(event) + } + } + + const mockModels: ModelRecord = { + "gpt-4o-mini": { + maxTokens: 4096, + contextWindow: 32768, + supportsImages: true, + supportsComputerUse: false, + supportsPromptCache: true, + supportsVerbosity: false, + supportsReasoningBudget: false, + supportsTemperature: true, + requiredReasoningBudget: false, + supportsReasoningEffort: false, + supportedParameters: ["max_tokens", "temperature"], + inputPrice: 0.00015, + outputPrice: 0.0006, + description: "GPT-4o mini model for testing", + displayName: "GPT-4o Mini", + preferredIndex: 1, + }, + "gpt-4o": { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + supportsVerbosity: false, + supportsReasoningBudget: false, + supportsTemperature: true, + requiredReasoningBudget: false, + supportsReasoningEffort: false, + supportedParameters: ["max_tokens", "temperature"], + inputPrice: 0.005, + outputPrice: 0.015, + cacheWritesPrice: 0.00625, + cacheReadsPrice: 0.00125, + description: "GPT-4o model for testing", + displayName: "GPT-4o", + preferredIndex: 2, + }, + } + + const mockDeployments: DeploymentRecord = { + "deployment-1": { + id: "deployment-1", + name: "GPT-4o Mini Deployment 1", + model: "gpt-4o-mini", + targetStatus: "RUNNING", + }, + "deployment-2": { + id: "deployment-2", + name: "GPT-4o Deployment 2", + model: "gpt-4o", + targetStatus: "STOPPED", + }, + "deployment-3": { + id: "deployment-3", + name: "GPT-4o Deployment 3", + model: "gpt-4o", + targetStatus: "RUNNING", + }, + } + + const mockDeploymentsWithoutGpt4o: DeploymentRecord = { + "deployment-1": { + id: "deployment-1", + name: "GPT-4o Mini Deployment 1", + model: "gpt-4o-mini", + targetStatus: "RUNNING", + }, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("Initial Rendering", () => { + it("should render all input fields with correct labels", () => { + render( + , + ) + + expect(screen.getByLabelText("settings:providers.sapAiCore.serviceKey")).toBeInTheDocument() + expect(screen.getByLabelText("settings:providers.sapAiCore.resourceGroup")).toBeInTheDocument() + }) + + it("should render orchestration checkbox unchecked by default", () => { + render( + , + ) + + const checkbox = screen.getByTestId("checkbox-sap-ai-core-orchestration") + expect(checkbox).not.toBeChecked() + }) + + it("should render model selector", () => { + render( + , + ) + + expect(screen.getByTestId("searchable-select")).toBeInTheDocument() + }) + + it("should render VSCode links", () => { + render( + , + ) + + const links = screen.getAllByTestId("vscode-link") + expect(links).toHaveLength(3) // credentials note, orchestration note, and get started + }) + }) + + describe("Input Field Behavior", () => { + it("should call setApiConfigurationField when input values change", async () => { + render( + , + ) + + const serviceKeyInput = screen.getByTestId("sap-ai-core-service-key") + + await act(async () => { + fireEvent.change(serviceKeyInput, { target: { value: "test-service-key" } }) + }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreServiceKey", "test-service-key") + }) + + it("should trigger model and deployment fetch when refetch fields change", async () => { + render( + , + ) + + const serviceKeyInput = screen.getByTestId("sap-ai-core-service-key") + await act(async () => { + fireEvent.change(serviceKeyInput, { target: { value: "test" } }) + }) + + // Should trigger initial fetch on mount and then again on input change + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "requestSapAiCoreModels", + values: expect.any(Object), + }) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "requestSapAiCoreDeployments", + values: expect.any(Object), + }) + }) + }) + + describe("Orchestration Mode", () => { + it("should toggle orchestration mode when checkbox is clicked", async () => { + render( + , + ) + + const checkbox = screen.getByTestId("checkbox-input-sap-ai-core-orchestration") + await act(async () => { + fireEvent.click(checkbox) + }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreUseOrchestration", true) + }) + + it("should clear model-related fields when toggling orchestration", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreModelId: "gpt-4o-mini", + sapAiCoreDeploymentId: "deployment-1", + } + + render( + , + ) + + const checkbox = screen.getByTestId("checkbox-input-sap-ai-core-orchestration") + await act(async () => { + fireEvent.click(checkbox) + }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreModelId", undefined) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreCustomModelInfo", undefined) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreDeploymentId", undefined) + }) + + it("should show warning message when orchestration is disabled", () => { + render( + , + ) + + expect(screen.getByText("settings:providers.sapAiCore.supportedProviders")).toBeInTheDocument() + expect(screen.getByText("settings:providers.sapAiCore.supportedProvidersDesc1")).toBeInTheDocument() + }) + + it("should hide warning message when orchestration is enabled", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: true, + } + + render( + , + ) + + expect(screen.queryByText("settings:providers.sapAiCore.supportedProviders")).not.toBeInTheDocument() + }) + }) + + describe("Model Deployment Description", () => { + it("should show model deployment description when orchestration is disabled and no model is selected", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + } + + render( + , + ) + + expect(screen.getByText("settings:providers.sapAiCore.modelDeploymentDesc")).toBeInTheDocument() + }) + + it("should hide model deployment description when orchestration is enabled", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: true, + } + + render( + , + ) + + expect(screen.queryByText("settings:providers.sapAiCore.modelDeploymentDesc")).not.toBeInTheDocument() + }) + + it("should hide model deployment description when model is selected", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + expect(screen.queryByText("settings:providers.sapAiCore.modelDeploymentDesc")).not.toBeInTheDocument() + }) + }) + + describe("Model Selection", () => { + it("should simulate receiving models from message handler", async () => { + render( + , + ) + + // Simulate message event using our helper + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + + // Wait for the models to be processed and check they are displayed + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (2)")).toBeInTheDocument() + }) + }) + + it("should handle model selection", async () => { + render( + , + ) + + // Simulate loading models + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + }) + + // Wait for models to be loaded + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (2)")).toBeInTheDocument() + }) + + // Clear previous mock calls from initialization + mockSetApiConfigurationField.mockClear() + + // Find and select a model + const modelSelect = screen.getByTestId("searchable-select") + await act(async () => { + fireEvent.change(modelSelect, { target: { value: "gpt-4o-mini" } }) + }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreModelId", "gpt-4o-mini") + }) + + it("should disable models without any deployments when orchestration is disabled", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + } + + render( + , + ) + + // Load models and deployments (using deployment data where gpt-4o has no deployments at all) + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeploymentsWithoutGpt4o, + }) + }) + + // Wait for models to be loaded + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (2)")).toBeInTheDocument() + }) + + // Check that the model selector has options with correct disabled states + const modelSelect = screen.getByTestId("searchable-select") + const options = modelSelect.querySelectorAll("option") + + // Find the gpt-4o option (should be disabled as it has no deployments) + const gpt4oOption = Array.from(options).find((option) => option.getAttribute("value") === "gpt-4o") + expect(gpt4oOption).toHaveProperty("disabled", true) + + // Find the gpt-4o-mini option (should be enabled as it has a deployment) + const gpt4oMiniOption = Array.from(options).find((option) => option.getAttribute("value") === "gpt-4o-mini") + expect(gpt4oMiniOption).toHaveProperty("disabled", false) + }) + + it("should enable all models when orchestration is enabled", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: true, + } + + render( + , + ) + + // Load models and deployments + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Wait for models to be loaded + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (2)")).toBeInTheDocument() + }) + + // Check that all models are enabled when orchestration is on + const modelSelect = screen.getByTestId("searchable-select") + const options = modelSelect.querySelectorAll("option") + + // All model options should be enabled (not disabled) + const modelOptions = Array.from(options).filter((option) => option.getAttribute("value") !== "") + modelOptions.forEach((option) => { + expect(option).not.toHaveAttribute("disabled") + }) + }) + + it("should set model info when model is selected", async () => { + render( + , + ) + + // Load models + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + }) + + // Clear previous mock calls + mockSetApiConfigurationField.mockClear() + + // Select a model + const modelSelect = screen.getByTestId("searchable-select") + await act(async () => { + fireEvent.change(modelSelect, { target: { value: "gpt-4o-mini" } }) + }) + + // Should set both model ID and custom model info + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreModelId", "gpt-4o-mini") + expect(mockSetApiConfigurationField).toHaveBeenCalledWith( + "sapAiCoreCustomModelInfo", + mockModels["gpt-4o-mini"], + ) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreDeploymentId", undefined) + }) + }) + + describe("Deployment Selection", () => { + it("should not show deployment selector when orchestration is enabled", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: true, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + // Should not show deployment selector when orchestration is enabled + expect(screen.queryByText("settings:providers.sapAiCore.deployment")).not.toBeInTheDocument() + }) + + it("should not show deployment selector when no model is selected", () => { + render( + , + ) + + // Should not show deployment selector when no model is selected + expect(screen.queryByText("settings:providers.sapAiCore.deployment")).not.toBeInTheDocument() + }) + + it("should show deployment selector when orchestration is disabled and model is selected", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + // Load deployments + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Should show deployment selector + expect(screen.getByText("settings:providers.sapAiCore.deployment")).toBeInTheDocument() + }) + + it("should show deployment count for selected model", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + // Load deployments + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Should show deployment count (1 deployment for gpt-4o-mini) + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.deploymentsCount (1)")).toBeInTheDocument() + }) + }) + + it("should handle deployment selection", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + // Load deployments + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Wait for deployment selector to appear + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.deployment")).toBeInTheDocument() + }) + + // Clear previous mock calls + mockSetApiConfigurationField.mockClear() + + // Find the deployment selector (it should be the second SearchableSelect) + const selects = screen.getAllByTestId("searchable-select") + const deploymentSelect = selects[1] // Second select is deployment select + + await act(async () => { + fireEvent.change(deploymentSelect, { target: { value: "deployment-1" } }) + }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreDeploymentId", "deployment-1") + }) + + it("should disable deployment selector when no deployments are available for selected model", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "model-without-deployments", + } + + render( + , + ) + + // Load deployments (none for this model) + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Wait for deployment selector to appear + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.deployment")).toBeInTheDocument() + }) + + // Find the deployment selector + const selects = screen.getAllByTestId("searchable-select") + const deploymentSelect = selects[1] + + expect(deploymentSelect).toBeDisabled() + }) + + it("should show correct placeholder text based on deployment availability", async () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreUseOrchestration: false, + sapAiCoreModelId: "gpt-4o-mini", + } + + const { rerender } = render( + , + ) + + // Load deployments + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Should show "Select a deployment..." placeholder + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.selectDeployment")).toBeInTheDocument() + }) + + // Change to model without deployments + const noDeploymentConfig = { + ...apiConfiguration, + sapAiCoreModelId: "model-without-deployments", + } + + rerender( + , + ) + + // Should show "No deployment found" placeholder + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.noDeploymentFound")).toBeInTheDocument() + }) + }) + }) + + describe("Loading States", () => { + it("should show loading state for models", () => { + render( + , + ) + + expect(screen.getByText("settings:providers.sapAiCore.loading")).toBeInTheDocument() + }) + + it("should disable model selector during loading", () => { + render( + , + ) + + const modelSelect = screen.getByTestId("searchable-select") + expect(modelSelect).toBeDisabled() + }) + + it("should show model count when models are loaded", async () => { + render( + , + ) + + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (2)")).toBeInTheDocument() + }) + }) + }) + + describe("ModelInfoView Integration", () => { + it("should show ModelInfoView when model is selected", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + expect(screen.getByTestId("model-info-view")).toBeInTheDocument() + }) + + it("should hide ModelInfoView when no model is selected", () => { + render( + , + ) + + expect(screen.queryByTestId("model-info-view")).not.toBeInTheDocument() + }) + + it("should handle description expansion toggle", () => { + const apiConfiguration = { + ...defaultApiConfiguration, + sapAiCoreModelId: "gpt-4o-mini", + } + + render( + , + ) + + const toggleButton = screen.getByTestId("model-info-toggle") + fireEvent.click(toggleButton) + + // The component should handle the state change internally + expect(toggleButton).toBeInTheDocument() + }) + }) + + describe("Edge Cases and Error Handling", () => { + it("should handle empty models response", async () => { + render( + , + ) + + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: {}, + }) + + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (0)")).toBeInTheDocument() + }) + }) + + it("should handle empty deployments response", async () => { + render( + , + ) + + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: {}, + }) + + // Since the current implementation doesn't show deployment counts, + // just verify component doesn't crash + expect(screen.getByTestId("searchable-select")).toBeInTheDocument() + }) + + it("should handle undefined models response", async () => { + render( + , + ) + + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: undefined, + }) + + // Should handle gracefully without crashing + expect(screen.getByTestId("searchable-select")).toBeInTheDocument() + }) + + it("should handle undefined deployments response", async () => { + render( + , + ) + + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: undefined, + }) + + // Should handle gracefully without crashing - only check for main model selector + expect(screen.getByTestId("searchable-select")).toBeInTheDocument() + }) + + it("should handle unknown message types", () => { + render( + , + ) + + // Should not crash on unknown message type + simulateMessageEvent({ + type: "unknownMessageType", + data: "some data", + }) + + expect(screen.getByTestId("searchable-select")).toBeInTheDocument() + }) + }) + + describe("Component Integration", () => { + it("should handle complete workflow", async () => { + render( + , + ) + + // Fill in credentials + const serviceKeyInput = screen.getByTestId("sap-ai-core-service-key") + await act(async () => { + fireEvent.change(serviceKeyInput, { target: { value: "test-service-key" } }) + }) + + // Load models and deployments + await act(async () => { + simulateMessageEvent({ + type: "sapAiCoreModels", + sapAiCoreModels: mockModels, + }) + simulateMessageEvent({ + type: "sapAiCoreDeployments", + sapAiCoreDeployments: mockDeployments, + }) + }) + + // Wait for models to be loaded + await waitFor(() => { + expect(screen.getByText("settings:providers.sapAiCore.modelsCount (2)")).toBeInTheDocument() + }) + + // Clear previous mock calls to focus on the model selection + mockSetApiConfigurationField.mockClear() + + // Select a model + const modelSelect = screen.getByTestId("searchable-select") + await act(async () => { + fireEvent.change(modelSelect, { target: { value: "gpt-4o-mini" } }) + }) + + // Verify model selection was called + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("sapAiCoreModelId", "gpt-4o-mini") + }) + + it("should properly handle refetch triggers", async () => { + render( + , + ) + + // Clear the initial fetch calls + mockPostMessage.mockClear() + + // Change a refetch field + const resourceGroupInput = screen.getByTestId("sap-ai-core-resource-group") + fireEvent.change(resourceGroupInput, { target: { value: "test-group" } }) + + // Should trigger new fetches + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "requestSapAiCoreModels", + values: expect.any(Object), + }) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "requestSapAiCoreDeployments", + values: expect.any(Object), + }) + }) + }) +}) diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 0f0483d9241..74a641b5f48 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -36,3 +36,4 @@ export { Synthetic } from "./Synthetic" // kilocode_change export { Featherless } from "./Featherless" export { VercelAiGateway } from "./VercelAiGateway" export { DeepInfra } from "./DeepInfra" +export { default as SapAiCore } from "./SapAiCore" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 8baa418b999..385f1b0c2f8 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -457,6 +457,17 @@ function getSelectedModel({ return { id, info } } // kilocode_change end + case "sap-ai-core": { + const id = apiConfiguration.sapAiCoreModelId ?? "gpt-5" + const info = { + maxTokens: 128000, + contextWindow: 400000, + supportsImages: true, + supportsPromptCache: true, + description: "GPT-5: The best model for coding and agentic tasks across domains", + } + return { id, info } + } // case "anthropic": // case "human-relay": // case "fake-ai": diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 069dc121aee..cffe45cb551 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -333,6 +333,32 @@ "huggingFaceSelectProvider": "Select a provider...", "huggingFaceSearchProviders": "Search providers...", "huggingFaceNoProvidersFound": "No providers found", + "sapAiCore": { + "serviceKey": "AI Core Service Key", + "serviceKeyJson": "Enter Service Key JSON...", + "resourceGroup": "AI Core Resource Group", + "credentialsNote": "These credentials are stored locally and only used to make API requests from this extension.", + "learnMore": "You can find more information about SAP AI Core API access here.", + "orchestrationMode": "Orchestration Mode", + "orchestrationEnabledDesc": "When enabled, provides access to all available models without requiring individual deployments.", + "orchestrationDisabledDesc": "When disabled, provides access only to deployed foundation models in your AI Core service instance.", + "modelDeploymentDesc": "You may only select models with available deployments.", + "deployment": "Deployment", + "loading": "Loading...", + "modelsCount": "({{count}} models)", + "deploymentsCount": "({{count}} deployments)", + "selectModel": "Select a model...", + "selectDeployment": "Select a deployment...", + "searchModels": "Search models...", + "searchDeployments": "Search deployments...", + "noModelsFound": "No models found", + "noDeploymentFound": "No deployment found", + "getStarted": "Get Started with SAP AI Core", + "supportedProviders": "Supported Providers", + "supportedProvidersDesc1": "The SAP AI Core provider currently only supports foundation models for the following LLM providers:", + "supportedProvidersDesc2": "Consider switching to Orchestration Mode for access to more models.", + "supportedProvidersDesc3": "Learn more about Orchestration Mode" + }, "getGeminiApiKey": "Get Gemini API Key", "openAiApiKey": "OpenAI API Key", "apiKey": "API Key", @@ -914,7 +940,9 @@ "providerNotAllowed": "Provider '{{provider}}' is not allowed by your organization", "modelNotAllowed": "Model '{{model}}' is not allowed for provider '{{provider}}' by your organization", "profileInvalid": "This profile contains a provider or model that is not allowed by your organization", - "qwenCodeOauthPath": "You must provide a valid OAuth credentials path." + "qwenCodeOauthPath": "You must provide a valid OAuth credentials path.", + "sapAiCore": "You must provide a valid SAP AI Core Service Key.", + "sapAiCoreDeploymentId": "You must select a running deployment for model '{{model}}'." }, "placeholders": { "apiKey": "Enter API Key...", diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.test.ts index f3484f3ff04..385a6e873be 100644 --- a/webview-ui/src/utils/__tests__/validate.test.ts +++ b/webview-ui/src/utils/__tests__/validate.test.ts @@ -66,6 +66,7 @@ describe("Model Validation Functions", () => { chutes: {}, gemini: {}, // kilocode_change end + "sap-ai-core": {}, } const allowAllOrganization: OrganizationAllowList = { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index c7bcbd25714..99ce7aacb8b 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -182,6 +182,19 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri } break // kilocode_change end + case "sap-ai-core": + if (!apiConfiguration.sapAiCoreServiceKey) { + return i18next.t("settings:validation.sapAiCore") + } + if (!apiConfiguration.sapAiCoreModelId) { + return i18next.t("settings:validation.modelId") + } + if (!apiConfiguration.sapAiCoreUseOrchestration && !apiConfiguration.sapAiCoreDeploymentId) { + return i18next.t("settings:validation.sapAiCoreDeploymentId", { + model: apiConfiguration.sapAiCoreModelId, + }) + } + break } return undefined