diff --git a/.gitignore b/.gitignore
index 4f4a2cf85..e76ef50b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,4 +118,5 @@ apps/*/
# Claude - session/user specific files
.claude/plans/
.claude/settings.local.json
-.claude/agent-memory/*
\ No newline at end of file
+.claude/agent-memory/*
+
diff --git a/packages/agentflow/README.md b/packages/agentflow/README.md
index 97f17c36b..f323f7641 100644
--- a/packages/agentflow/README.md
+++ b/packages/agentflow/README.md
@@ -1,270 +1,270 @@
-# @flowiseai/agentflow
-
-[](https://www.npmjs.com/package/@flowiseai/agentflow)
-[](https://github.com/FlowiseAI/Flowise/blob/main/LICENSE.md)
-
-> Embeddable React component for building and visualizing AI agent workflows
-
-## Status: Dev
-
-**Current version: `0.0.0-dev.13`**
-
-This package is functional and has comprehensive test coverage, but the public API may still change before a stable release. It is suitable for early integration and testing but not yet recommended for production use.
-
-**What works today:**
-
-- 13 node types with full editing support (Loop and Human Input nodes are present in the palette but not yet fully verified end-to-end)
-- Connection validation, flow validation, and export
-- Async option loading (models, credentials, tools)
-- Variable picker and field visibility conditions
-- AI flow generation from natural language
-- Dark/light mode theming
-- Read-only mode and custom rendering via render props
-
-## Overview
-
-`@flowiseai/agentflow` is a React-based flow editor for creating AI agent workflows. It provides a visual canvas built on ReactFlow for connecting AI agents, LLMs, tools, and logic nodes.
-
-## Features
-
-- **Visual Canvas** — Drag-and-drop flow editor built on ReactFlow with zoom, pan, minimap, and fit-to-view controls
-- **13 Built-in Node Types** — Start, Agent, LLM, Condition, Condition Agent, Direct Reply, Custom Function, Tool, Retriever, Sticky Note, HTTP, Iteration, and Execute Flow
-- **Node Editor Dialog** — Modal for editing node parameters with dynamic input types (text, number, boolean, dropdown, arrays, JSON, code, variable selector, async options)
-- **Credential Management** — Create and edit credentials inline from the node editor
-- **Rich Text Editor** — TipTap-based editor with syntax highlighting for JavaScript, TypeScript, Python, and JSON (lazy-loaded)
-- **Specialized Input Components** — Condition builder, messages input (role + content), and structured output schema builder
-- **AI Flow Generator** — Generate flows from natural language descriptions with model selection
-- **Flow Validation** — Detects empty flows, missing start nodes, disconnected nodes, cycles, hanging edges, and per-node input errors with visual feedback
-- **Dark Mode** — Full light/dark theme support via design tokens and CSS variables
-- **Read-Only Mode** — Disable editing for view-only embedding
-- **Custom Rendering** — Replace the default header and node palette with your own components via render props
-- **Imperative API** — Programmatic control via ref (`getFlow`, `validate`, `fitView`, `clear`, `addNode`, `toJSON`)
-- **Request Interceptor** — Customize outgoing API requests (headers, credentials) via an Axios interceptor callback
-- **Keyboard Shortcuts** — Cmd/Ctrl+S to save
-
-## Installation
-
-```bash
-pnpm add @flowiseai/agentflow
-```
-
-**Peer Dependencies:**
-
-```bash
-pnpm add react react-dom @mui/material @mui/icons-material @emotion/react @emotion/styled reactflow
-```
-
-## Basic Usage
-
-```tsx
-import { Agentflow } from '@flowiseai/agentflow'
-
-import '@flowiseai/agentflow/flowise.css'
-
-export default function App() {
- return (
-
- )
-}
-```
-
-### With Initial Flow Data and Callbacks
-
-```tsx
-import { useRef } from 'react'
-
-import { Agentflow, type AgentFlowInstance, type FlowData } from '@flowiseai/agentflow'
-
-import '@flowiseai/agentflow/flowise.css'
-
-export default function App() {
- const ref = useRef(null)
-
- const initialFlow: FlowData = {
- nodes: [
- {
- id: 'startAgentflow_0',
- type: 'agentflowNode',
- position: { x: 100, y: 100 },
- data: {
- id: 'startAgentflow_0',
- name: 'startAgentflow',
- label: 'Start',
- color: '#7EE787',
- hideInput: true,
- outputAnchors: [{ id: 'startAgentflow_0-output-0', name: 'start', label: 'Start', type: 'start' }]
- }
- }
- ],
- edges: [],
- viewport: { x: 0, y: 0, zoom: 1 }
- }
-
- return (
-
-
console.log('Flow changed:', flow)}
- onSave={(flow) => console.log('Flow saved:', flow)}
- />
-
- )
-}
-```
-
-### More Examples
-
-The [examples app](./examples/README.md) includes working demos for:
-
-- **Variable usage** — `{{variable}}` syntax, variable picker, available sources
-- **Async options** — Loading models, tools, and credentials from the API
-- **Status indicators** — Node execution states (running, finished, error, stopped)
-- **Field visibility** — Show/hide conditions on node inputs
-- **State management** — Dirty tracking, flow change callbacks
-- **Dark mode** — Light/dark theme toggle
-- **Custom UI** — Custom header and node palette via render props
-- **Filtered components** — Restricting available node types with presets
-- **Validation actions** — Validation button, error display, and custom `canvasActions` alongside built-in controls
-
-Run `cd examples && npm install && npm run dev` to try them locally.
-
-## Props
-
-
-| Prop | Type | Default | Description |
-| -------------------- | ------------------------------------------ | -------------- | --------------------------------------------------------------- |
-| `apiBaseUrl` | `string` | **(required)** | Flowise API server endpoint |
-| `token` | `string` | — | Authentication token for API calls |
-| `requestInterceptor` | `(config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig` | — | Customize outgoing API requests (e.g., set `withCredentials`, add headers). The callback receives the full Axios request config — only modify what you need. See [Security: requestInterceptor](#security-requestinterceptor) below. |
-| `initialFlow` | `FlowData` | — | Initial flow data to render (uncontrolled — only used on mount) |
-| `flowId` | `string` | — | Flow identifier (reserved for future use) |
-| `components` | `string[]` | — | Restrict which node types appear in the palette |
-| `onFlowChange` | `(flow: FlowData) => void` | — | Called when the flow changes (node/edge add, remove, move) |
-| `onSave` | `(flow: FlowData) => void` | — | Called when the user triggers a save |
-| `onFlowGenerated` | `(flow: FlowData) => void` | — | Called when a flow is generated via AI |
-| `isDarkMode` | `boolean` | `false` | Use dark mode theme |
-| `readOnly` | `boolean` | `false` | Disable editing (nodes not draggable/connectable) |
-| `showDefaultHeader` | `boolean` | `true` | Show built-in header (ignored if `renderHeader` provided) |
-| `showDefaultPalette` | `boolean` | `true` | Show built-in node palette |
-| `enableGenerator` | `boolean` | `true` | Show the AI flow generator button |
-| `canvasActions` | `ReactNode` | — | Additional content rendered in the top-right canvas overlay, to the right of the built-in validation FAB. Hidden when `readOnly` is true. |
-| `renderHeader` | `(props: HeaderRenderProps) => ReactNode` | — | Custom header renderer |
-| `renderNodePalette` | `(props: PaletteRenderProps) => ReactNode` | — | Custom node palette renderer |
-
-### Imperative Methods (via `ref`)
-
-| Method | Return Type | Description |
-| ------------------------ | ------------------------- | ------------------------------------- |
-| `getFlow()` | `FlowData` | Get current flow data |
-| `toJSON()` | `string` | Export flow as JSON string |
-| `validate()` | `ValidationResult` | Validate the current flow |
-| `fitView()` | `void` | Fit all nodes into view |
-| `clear()` | `void` | Remove all nodes and edges |
-| `addNode(nodeData)` | `void` | Add a node (`Partial`) |
-| `getReactFlowInstance()` | `ReactFlowInstance\|null` | Get the underlying ReactFlow instance |
-
-### Security: `requestInterceptor`
-
-The `requestInterceptor` callback runs inside the Axios request pipeline and has access to the full request configuration, including authentication headers. This is the same trust model as any other callback prop (e.g., `onSave`, `renderHeader`) — the host application developer supplies the function and is responsible for its behavior.
-
-**Guidelines for consumers:**
-
-- Only pass **trusted, developer-authored** functions. Never use dynamically evaluated code (`eval`, `new Function`, etc.) or user-generated input as the interceptor.
-- Follow the **principle of least privilege** — only read or modify the specific config properties you need (e.g., `withCredentials`, custom headers).
-- If the interceptor throws, the error is caught, logged, and the **original unmodified config** is used so the request still proceeds safely.
-
-### Node Types
-
-The following node types are available in the palette by default. Use the `components` prop to restrict which types are shown.
-
-
-| Node Type | Description |
-| -------------------------- | ------------------------------------ |
-| `startAgentflow` | Entry point (required, always shown) |
-| `agentAgentflow` | AI agent execution |
-| `llmAgentflow` | LLM / language model call |
-| `conditionAgentflow` | Conditional branching |
-| `conditionAgentAgentflow` | Agent-level conditional branching |
-| `directReplyAgentflow` | Direct response to user |
-| `customFunctionAgentflow` | Custom JavaScript function |
-| `toolAgentflow` | Tool integration |
-| `retrieverAgentflow` | Data retrieval |
-| `stickyNoteAgentflow` | Canvas annotation (not connectable) |
-| `httpAgentflow` | HTTP request |
-| `iterationAgentflow` | Iteration / map-reduce container |
-| `executeFlowAgentflow` | Execute a sub-flow |
-
-### Design Note
-
-`` is an **uncontrolled component**. The `initialFlow` prop seeds the canvas state on mount, but the component owns its own state afterward. Use the `ref` for imperative access and `onFlowChange` to observe changes.
-
-## Development
-
-```bash
-# Install dependencies
-pnpm install
-
-# Build the package
-pnpm build
-
-# Run tests
-pnpm test
-
-# Run examples
-cd examples && pnpm install && pnpm dev
-```
-
-Visit the [examples](./examples) directory for more usage patterns. See [TESTS.md](./TESTS.md) for the full test plan and coverage status.
-
-## Troubleshooting
-
-### API Connection Issues
-
-- **CORS errors** — Ensure the Flowise server allows requests from your app's origin. Check the Flowise CORS configuration.
-- **401 Unauthorized** — Use an API Key (Settings > API Keys), not a JWT user token. Verify the key is passed via the `token` prop.
-- **Wrong `apiBaseUrl`** — The URL must point to the Flowise API root (e.g., `http://localhost:3000`), not a subpath.
-
-### Validation Errors
-
-- **"Flow is empty"** — Add at least one node to the canvas.
-- **"Missing start node"** — Every flow requires a `startAgentflow` node. Add one from the palette.
-- **"Disconnected nodes"** — All non-sticky-note nodes must be reachable from the start node. Connect any orphaned nodes.
-- **"Cycle detected"** — Flows must be acyclic (DAGs). Remove the edge that creates the cycle.
-
-### Theme Issues
-
-- **Dark mode not applying** — Pass `isDarkMode={true}` as a prop. The component uses its own design tokens and does not inherit from the host app's theme.
-- **Style conflicts** — Ensure `@flowiseai/agentflow/flowise.css` is imported. The component's CSS variables are scoped to avoid collisions.
-
-### Variables Not Resolving
-
-- **Variable not in picker** — Variables are sourced from upstream nodes. Ensure the source node is connected and upstream of the current node.
-- **Incorrect path** — Variable syntax is `{{nodeName.outputKey}}`. Check that the node name and output key match exactly.
-
-### Async Options Not Loading
-
-- **Empty dropdowns for models/tools/credentials** — These load from the Flowise API. Verify `apiBaseUrl` and `token` are correct and the server is running.
-- **Network errors in console** — Check browser DevTools for failed requests. The API client logs errors to the console.
-
-## Documentation
-
-- [ARCHITECTURE.md](./ARCHITECTURE.md) - Internal architecture and design patterns
-- [TESTS.md](./TESTS.md) - Test plan, coverage tiers, and configuration
-- [Examples](./examples/README.md) - Usage examples and demos
-
-## Contributing
-
-This package follows a feature-based architecture with clear separation of concerns. See [ARCHITECTURE.md](./ARCHITECTURE.md) for details on the project structure and development guidelines.
-
-## License
-
-Apache-2.0 — see the repository root [LICENSE.md](https://github.com/FlowiseAI/Flowise/blob/main/LICENSE.md) for details.
-
----
-
-Part of the [Flowise](https://github.com/FlowiseAI/Flowise) ecosystem
+# @flowiseai/agentflow
+
+[](https://www.npmjs.com/package/@flowiseai/agentflow)
+[](https://github.com/FlowiseAI/Flowise/blob/main/LICENSE.md)
+
+> Embeddable React component for building and visualizing AI agent workflows
+
+## Status: Dev
+
+**Current version: `0.0.0-dev.13`**
+
+This package is functional and has comprehensive test coverage, but the public API may still change before a stable release. It is suitable for early integration and testing but not yet recommended for production use.
+
+**What works today:**
+
+- 13 node types with full editing support (Loop and Human Input nodes are present in the palette but not yet fully verified end-to-end)
+- Connection validation, flow validation, and export
+- Async option loading (models, credentials, tools)
+- Variable picker and field visibility conditions
+- AI flow generation from natural language
+- Dark/light mode theming
+- Read-only mode and custom rendering via render props
+
+## Overview
+
+`@flowiseai/agentflow` is a React-based flow editor for creating AI agent workflows. It provides a visual canvas built on ReactFlow for connecting AI agents, LLMs, tools, and logic nodes.
+
+## Features
+
+- **Visual Canvas** — Drag-and-drop flow editor built on ReactFlow with zoom, pan, minimap, and fit-to-view controls
+- **13 Built-in Node Types** — Start, Agent, LLM, Condition, Condition Agent, Direct Reply, Custom Function, Tool, Retriever, Sticky Note, HTTP, Iteration, and Execute Flow
+- **Node Editor Dialog** — Modal for editing node parameters with dynamic input types (text, number, boolean, dropdown, arrays, JSON, code, variable selector, async options)
+- **Credential Management** — Create and edit credentials inline from the node editor
+- **Rich Text Editor** — TipTap-based editor with syntax highlighting for JavaScript, TypeScript, Python, and JSON (lazy-loaded)
+- **Specialized Input Components** — Condition builder, messages input (role + content), and structured output schema builder
+- **AI Flow Generator** — Generate flows from natural language descriptions with model selection
+- **Flow Validation** — Detects empty flows, missing start nodes, disconnected nodes, cycles, hanging edges, and per-node input errors with visual feedback
+- **Dark Mode** — Full light/dark theme support via design tokens and CSS variables
+- **Read-Only Mode** — Disable editing for view-only embedding
+- **Custom Rendering** — Replace the default header and node palette with your own components via render props
+- **Imperative API** — Programmatic control via ref (`getFlow`, `validate`, `fitView`, `clear`, `addNode`, `toJSON`)
+- **Request Interceptor** — Customize outgoing API requests (headers, credentials) via an Axios interceptor callback
+- **Keyboard Shortcuts** — Cmd/Ctrl+S to save
+
+## Installation
+
+```bash
+pnpm add @flowiseai/agentflow
+```
+
+**Peer Dependencies:**
+
+```bash
+pnpm add react react-dom @mui/material @mui/icons-material @emotion/react @emotion/styled reactflow
+```
+
+## Basic Usage
+
+```tsx
+import { Agentflow } from '@flowiseai/agentflow'
+
+import '@flowiseai/agentflow/flowise.css'
+
+export default function App() {
+ return (
+
+ )
+}
+```
+
+### With Initial Flow Data and Callbacks
+
+```tsx
+import { useRef } from 'react'
+
+import { Agentflow, type AgentFlowInstance, type FlowData } from '@flowiseai/agentflow'
+
+import '@flowiseai/agentflow/flowise.css'
+
+export default function App() {
+ const ref = useRef(null)
+
+ const initialFlow: FlowData = {
+ nodes: [
+ {
+ id: 'startAgentflow_0',
+ type: 'agentflowNode',
+ position: { x: 100, y: 100 },
+ data: {
+ id: 'startAgentflow_0',
+ name: 'startAgentflow',
+ label: 'Start',
+ color: '#7EE787',
+ hideInput: true,
+ outputAnchors: [{ id: 'startAgentflow_0-output-0', name: 'start', label: 'Start', type: 'start' }]
+ }
+ }
+ ],
+ edges: [],
+ viewport: { x: 0, y: 0, zoom: 1 }
+ }
+
+ return (
+
+
console.log('Flow changed:', flow)}
+ onSave={(flow) => console.log('Flow saved:', flow)}
+ />
+
+ )
+}
+```
+
+### More Examples
+
+The [examples app](./examples/README.md) includes working demos for:
+
+- **Variable usage** — `{{variable}}` syntax, variable picker, available sources
+- **Async options** — Loading models, tools, and credentials from the API
+- **Status indicators** — Node execution states (running, finished, error, stopped)
+- **Field visibility** — Show/hide conditions on node inputs
+- **State management** — Dirty tracking, flow change callbacks
+- **Dark mode** — Light/dark theme toggle
+- **Custom UI** — Custom header and node palette via render props
+- **Filtered components** — Restricting available node types with presets
+- **Validation actions** — Validation button, error display, and custom `canvasActions` alongside built-in controls
+
+Run `cd examples && npm install && npm run dev` to try them locally.
+
+## Props
+
+
+| Prop | Type | Default | Description |
+| -------------------- | ------------------------------------------ | -------------- | --------------------------------------------------------------- |
+| `apiBaseUrl` | `string` | **(required)** | Flowise API server endpoint |
+| `token` | `string` | — | Authentication token for API calls |
+| `requestInterceptor` | `(config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig` | — | Customize outgoing API requests (e.g., set `withCredentials`, add headers). The callback receives the full Axios request config — only modify what you need. See [Security: requestInterceptor](#security-requestinterceptor) below. |
+| `initialFlow` | `FlowData` | — | Initial flow data to render (uncontrolled — only used on mount) |
+| `flowId` | `string` | — | Flow identifier (reserved for future use) |
+| `components` | `string[]` | — | Restrict which node types appear in the palette |
+| `onFlowChange` | `(flow: FlowData) => void` | — | Called when the flow changes (node/edge add, remove, move) |
+| `onSave` | `(flow: FlowData) => void` | — | Called when the user triggers a save |
+| `onFlowGenerated` | `(flow: FlowData) => void` | — | Called when a flow is generated via AI |
+| `isDarkMode` | `boolean` | `false` | Use dark mode theme |
+| `readOnly` | `boolean` | `false` | Disable editing (nodes not draggable/connectable) |
+| `showDefaultHeader` | `boolean` | `true` | Show built-in header (ignored if `renderHeader` provided) |
+| `showDefaultPalette` | `boolean` | `true` | Show built-in node palette |
+| `enableGenerator` | `boolean` | `true` | Show the AI flow generator button |
+| `canvasActions` | `ReactNode` | — | Additional content rendered in the top-right canvas overlay, to the right of the built-in validation FAB. Hidden when `readOnly` is true. |
+| `renderHeader` | `(props: HeaderRenderProps) => ReactNode` | — | Custom header renderer |
+| `renderNodePalette` | `(props: PaletteRenderProps) => ReactNode` | — | Custom node palette renderer |
+
+### Imperative Methods (via `ref`)
+
+| Method | Return Type | Description |
+| ------------------------ | ------------------------- | ------------------------------------- |
+| `getFlow()` | `FlowData` | Get current flow data |
+| `toJSON()` | `string` | Export flow as JSON string |
+| `validate()` | `ValidationResult` | Validate the current flow |
+| `fitView()` | `void` | Fit all nodes into view |
+| `clear()` | `void` | Remove all nodes and edges |
+| `addNode(nodeData)` | `void` | Add a node (`Partial`) |
+| `getReactFlowInstance()` | `ReactFlowInstance\|null` | Get the underlying ReactFlow instance |
+
+### Security: `requestInterceptor`
+
+The `requestInterceptor` callback runs inside the Axios request pipeline and has access to the full request configuration, including authentication headers. This is the same trust model as any other callback prop (e.g., `onSave`, `renderHeader`) — the host application developer supplies the function and is responsible for its behavior.
+
+**Guidelines for consumers:**
+
+- Only pass **trusted, developer-authored** functions. Never use dynamically evaluated code (`eval`, `new Function`, etc.) or user-generated input as the interceptor.
+- Follow the **principle of least privilege** — only read or modify the specific config properties you need (e.g., `withCredentials`, custom headers).
+- If the interceptor throws, the error is caught, logged, and the **original unmodified config** is used so the request still proceeds safely.
+
+### Node Types
+
+The following node types are available in the palette by default. Use the `components` prop to restrict which types are shown.
+
+
+| Node Type | Description |
+| -------------------------- | ------------------------------------ |
+| `startAgentflow` | Entry point (required, always shown) |
+| `agentAgentflow` | AI agent execution |
+| `llmAgentflow` | LLM / language model call |
+| `conditionAgentflow` | Conditional branching |
+| `conditionAgentAgentflow` | Agent-level conditional branching |
+| `directReplyAgentflow` | Direct response to user |
+| `customFunctionAgentflow` | Custom JavaScript function |
+| `toolAgentflow` | Tool integration |
+| `retrieverAgentflow` | Data retrieval |
+| `stickyNoteAgentflow` | Canvas annotation (not connectable) |
+| `httpAgentflow` | HTTP request |
+| `iterationAgentflow` | Iteration / map-reduce container |
+| `executeFlowAgentflow` | Execute a sub-flow |
+
+### Design Note
+
+`` is an **uncontrolled component**. The `initialFlow` prop seeds the canvas state on mount, but the component owns its own state afterward. Use the `ref` for imperative access and `onFlowChange` to observe changes.
+
+## Development
+
+```bash
+# Install dependencies
+pnpm install
+
+# Build the package
+pnpm build
+
+# Run tests
+pnpm test
+
+# Run examples
+cd examples && pnpm install && pnpm dev
+```
+
+Visit the [examples](./examples) directory for more usage patterns. See [TESTS.md](./TESTS.md) for the full test plan and coverage status.
+
+## Troubleshooting
+
+### API Connection Issues
+
+- **CORS errors** — Ensure the Flowise server allows requests from your app's origin. Check the Flowise CORS configuration.
+- **401 Unauthorized** — Use an API Key (Settings > API Keys), not a JWT user token. Verify the key is passed via the `token` prop.
+- **Wrong `apiBaseUrl`** — The URL must point to the Flowise API root (e.g., `http://localhost:3000`), not a subpath.
+
+### Validation Errors
+
+- **"Flow is empty"** — Add at least one node to the canvas.
+- **"Missing start node"** — Every flow requires a `startAgentflow` node. Add one from the palette.
+- **"Disconnected nodes"** — All non-sticky-note nodes must be reachable from the start node. Connect any orphaned nodes.
+- **"Cycle detected"** — Flows must be acyclic (DAGs). Remove the edge that creates the cycle.
+
+### Theme Issues
+
+- **Dark mode not applying** — Pass `isDarkMode={true}` as a prop. The component uses its own design tokens and does not inherit from the host app's theme.
+- **Style conflicts** — Ensure `@flowiseai/agentflow/flowise.css` is imported. The component's CSS variables are scoped to avoid collisions.
+
+### Variables Not Resolving
+
+- **Variable not in picker** — Variables are sourced from upstream nodes. Ensure the source node is connected and upstream of the current node.
+- **Incorrect path** — Variable syntax is `{{nodeName.outputKey}}`. Check that the node name and output key match exactly.
+
+### Async Options Not Loading
+
+- **Empty dropdowns for models/tools/credentials** — These load from the Flowise API. Verify `apiBaseUrl` and `token` are correct and the server is running.
+- **Network errors in console** — Check browser DevTools for failed requests. The API client logs errors to the console.
+
+## Documentation
+
+- [ARCHITECTURE.md](./ARCHITECTURE.md) - Internal architecture and design patterns
+- [TESTS.md](./TESTS.md) - Test plan, coverage tiers, and configuration
+- [Examples](./examples/README.md) - Usage examples and demos
+
+## Contributing
+
+This package follows a feature-based architecture with clear separation of concerns. See [ARCHITECTURE.md](./ARCHITECTURE.md) for details on the project structure and development guidelines.
+
+## License
+
+Apache-2.0 — see the repository root [LICENSE.md](https://github.com/FlowiseAI/Flowise/blob/main/LICENSE.md) for details.
+
+---
+
+Part of the [Flowise](https://github.com/FlowiseAI/Flowise) ecosystem
diff --git a/packages/agentflow/src/features/canvas/components/NodeModelConfigs.tsx b/packages/agentflow/src/features/canvas/components/NodeModelConfigs.tsx
index 48ffa0ba3..bfa57ca49 100644
--- a/packages/agentflow/src/features/canvas/components/NodeModelConfigs.tsx
+++ b/packages/agentflow/src/features/canvas/components/NodeModelConfigs.tsx
@@ -6,7 +6,7 @@ import { useApiContext, useConfigContext } from '@/infrastructure/store'
interface ModelConfig {
model?: string
- config?: { modelName?: string; model?: string }
+ config?: { modelName?: string; model?: string; customModel?: string }
}
export interface NodeModelConfigsProps {
@@ -54,7 +54,11 @@ function NodeModelConfigsComponent({ inputs }: NodeModelConfigsProps) {
src={`${apiBaseUrl}/api/v1/node-icon/${item.model}`}
alt={item.model as string}
/>
- {item.config?.modelName || item.config?.model}
+
+ {item.config?.customModel
+ ? item.config.customModel.replace(/^arn:aws:bedrock:[^:]+:[^:]+:/, '')
+ : item.config?.modelName || item.config?.model}
+
))}
diff --git a/packages/components/evaluation/EvaluationRunTracer.ts b/packages/components/evaluation/EvaluationRunTracer.ts
index ce286eb52..924774e89 100644
--- a/packages/components/evaluation/EvaluationRunTracer.ts
+++ b/packages/components/evaluation/EvaluationRunTracer.ts
@@ -66,7 +66,7 @@ export class EvaluationRunTracer extends RunCollectorCallbackHandler {
onLLMEnd?(run: Run): void | Promise {
if (run.name) {
let provider = run.name
- if (provider === 'BedrockChat') {
+ if (provider === 'BedrockChat' || provider === 'bedrock-imported') {
provider = 'awsChatBedrock'
}
EvaluationRunner.addMetrics(
diff --git a/packages/components/models.json b/packages/components/models.json
index c750134bf..fa65dc763 100644
--- a/packages/components/models.json
+++ b/packages/components/models.json
@@ -4,197 +4,542 @@
"name": "awsChatBedrock",
"models": [
{
- "label": "anthropic.claude-opus-4-7-v1",
- "name": "anthropic.claude-opus-4-7-v1",
- "description": "Claude 4.7 Opus",
- "input_cost": 0.000005,
- "output_cost": 0.000025
+ "label": "ai21.jamba-1-5-large-v1:0",
+ "name": "ai21.jamba-1-5-large-v1:0",
+ "description": "AI21 Jamba 1.5 Large - Text generation",
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
},
{
- "label": "anthropic.claude-opus-4-6-v1",
- "name": "anthropic.claude-opus-4-6-v1",
- "description": "Claude 4.6 Opus",
- "input_cost": 0.000005,
- "output_cost": 0.000025
+ "label": "ai21.jamba-1-5-mini-v1:0",
+ "name": "ai21.jamba-1-5-mini-v1:0",
+ "description": "AI21 Jamba 1.5 Mini - Text generation",
+ "input_cost": 0.0000002,
+ "output_cost": 0.0000004
},
{
- "label": "anthropic.claude-sonnet-4-6",
- "name": "anthropic.claude-sonnet-4-6",
- "description": "Claude 4.6 Sonnet",
- "input_cost": 0.000003,
- "output_cost": 0.000015
+ "label": "amazon.nova-micro-v1:0",
+ "name": "amazon.nova-micro-v1:0",
+ "description": "Amazon Nova Micro - Text-only, fastest and most cost-effective",
+ "input_cost": 0.000000035,
+ "output_cost": 0.00000014,
+ "inference_profile_geos": ["us", "eu", "apac"]
},
{
- "label": "anthropic.claude-opus-4-5-20251101-v1:0",
- "name": "anthropic.claude-opus-4-5-20251101-v1:0",
- "description": "Claude 4.5 Opus",
- "input_cost": 0.000005,
- "output_cost": 0.000025
+ "label": "amazon.nova-lite-v1:0",
+ "name": "amazon.nova-lite-v1:0",
+ "description": "Amazon Nova Lite - Multimodal (text, image, video)",
+ "input_cost": 0.00000006,
+ "output_cost": 0.00000024,
+ "inference_profile_geos": ["us", "eu", "apac", "ca"]
},
{
- "label": "anthropic.claude-sonnet-4-5-20250929-v1:0",
- "name": "anthropic.claude-sonnet-4-5-20250929-v1:0",
- "description": "Claude 4.5 Sonnet",
- "input_cost": 0.000003,
- "output_cost": 0.000015
+ "label": "amazon.nova-pro-v1:0",
+ "name": "amazon.nova-pro-v1:0",
+ "description": "Amazon Nova Pro - Multimodal (text, image, video), balanced performance",
+ "input_cost": 0.0000008,
+ "output_cost": 0.0000032,
+ "inference_profile_geos": ["us", "eu", "apac"]
+ },
+ {
+ "label": "amazon.nova-2-lite-v1:0",
+ "name": "amazon.nova-2-lite-v1:0",
+ "description": "Amazon Nova 2 Lite - Multimodal (text, image, video)",
+ "input_cost": 0.00000006,
+ "output_cost": 0.00000024,
+ "inference_profile_geos": ["us", "eu", "jp", "global"]
},
{
"label": "anthropic.claude-haiku-4-5-20251001-v1:0",
"name": "anthropic.claude-haiku-4-5-20251001-v1:0",
- "description": "Claude 4.5 Haiku",
+ "description": "Claude 4.5 Haiku - Latest Haiku with multimodal support",
"input_cost": 0.000001,
- "output_cost": 0.000005
- },
- {
- "label": "openai.gpt-oss-20b-1:0",
- "name": "openai.gpt-oss-20b-1:0",
- "description": "21B parameters model optimized for lower latency, local, and specialized use cases",
- "input_cost": 0.00007,
- "output_cost": 0.0003
- },
- {
- "label": "openai.gpt-oss-120b-1:0",
- "name": "openai.gpt-oss-120b-1:0",
- "description": "120B parameters model optimized for production, general purpose, and high-reasoning use cases",
- "input_cost": 0.00015,
- "output_cost": 0.0006
- },
- {
- "label": "anthropic.claude-opus-4-1-20250805-v1:0",
- "name": "anthropic.claude-opus-4-1-20250805-v1:0",
- "description": "Claude 4.1 Opus",
- "input_cost": 0.000015,
- "output_cost": 0.000075
+ "output_cost": 0.000005,
+ "inference_profile_geos": ["us", "eu", "jp", "au", "global"]
},
{
"label": "anthropic.claude-sonnet-4-20250514-v1:0",
"name": "anthropic.claude-sonnet-4-20250514-v1:0",
- "description": "Claude 4 Sonnet",
+ "description": "Claude 4 Sonnet - Balanced intelligence and speed, multimodal",
"input_cost": 0.000003,
- "output_cost": 0.000015
+ "output_cost": 0.000015,
+ "inference_profile_geos": ["us", "eu", "apac", "global"]
},
{
- "label": "anthropic.claude-opus-4-20250514-v1:0",
- "name": "anthropic.claude-opus-4-20250514-v1:0",
- "description": "Claude 4 Opus",
- "input_cost": 0.000015,
- "output_cost": 0.000075
- },
- {
- "label": "anthropic.claude-3-7-sonnet-20250219-v1:0",
- "name": "anthropic.claude-3-7-sonnet-20250219-v1:0",
- "description": "(20250219-v1:0) specific version of Claude Sonnet 3.7",
- "input_cost": 3e-6,
- "output_cost": 0.000015
- },
- {
- "label": "anthropic.claude-3-5-haiku-20241022-v1:0",
- "name": "anthropic.claude-3-5-haiku-20241022-v1:0",
- "description": "(20241022-v1:0) specific version of Claude Haiku 3.5",
- "input_cost": 8e-7,
- "output_cost": 4e-6
- },
- {
- "label": "anthropic.claude-3-5-sonnet-20241022-v2:0",
- "name": "anthropic.claude-3-5-sonnet-20241022-v2:0",
- "description": "(20241022-v2:0) specific version of Claude Sonnet 3.5",
- "input_cost": 3e-6,
- "output_cost": 0.000015
- },
- {
- "label": "anthropic.claude-3-5-sonnet-20240620-v1:0",
- "name": "anthropic.claude-3-5-sonnet-20240620-v1:0",
- "description": "(20240620-v1:0) specific version of Claude Sonnet 3.5",
- "input_cost": 3e-6,
- "output_cost": 0.000015
- },
- {
- "label": "anthropic.claude-3-opus",
- "name": "anthropic.claude-3-opus-20240229-v1:0",
- "input_cost": 0.000015,
- "output_cost": 0.000075
- },
- {
- "label": "anthropic.claude-3-sonnet",
- "name": "anthropic.claude-3-sonnet-20240229-v1:0",
+ "label": "anthropic.claude-sonnet-4-5-20250929-v1:0",
+ "name": "anthropic.claude-sonnet-4-5-20250929-v1:0",
+ "description": "Claude 4.5 Sonnet - Enhanced reasoning, multimodal",
"input_cost": 0.000003,
- "output_cost": 0.000015
+ "output_cost": 0.000015,
+ "inference_profile_geos": ["us", "eu", "jp", "au", "global"]
},
{
- "label": "anthropic.claude-3-haiku",
- "name": "anthropic.claude-3-haiku-20240307-v1:0",
- "input_cost": 2.5e-7,
- "output_cost": 1.25e-6
+ "label": "anthropic.claude-sonnet-4-6",
+ "name": "anthropic.claude-sonnet-4-6",
+ "description": "Claude 4.6 Sonnet - Latest Sonnet, multimodal",
+ "input_cost": 0.000003,
+ "output_cost": 0.000015,
+ "inference_profile_geos": ["us", "eu", "jp", "au", "global"]
},
{
- "label": "anthropic.claude-instant-v1",
- "name": "anthropic.claude-instant-v1",
- "description": "Text generation, conversation",
- "input_cost": 0.000008,
- "output_cost": 0.000024
+ "label": "anthropic.claude-opus-4-1-20250805-v1:0",
+ "name": "anthropic.claude-opus-4-1-20250805-v1:0",
+ "description": "Claude 4.1 Opus - Powerful reasoning, multimodal",
+ "input_cost": 0.000015,
+ "output_cost": 0.000075,
+ "inference_profile_geos": ["us"]
},
{
- "label": "anthropic.claude-v2:1",
- "name": "anthropic.claude-v2:1",
- "description": "Text generation, conversation, complex reasoning and analysis",
- "input_cost": 0.000008,
- "output_cost": 0.000024
+ "label": "anthropic.claude-opus-4-5-20251101-v1:0",
+ "name": "anthropic.claude-opus-4-5-20251101-v1:0",
+ "description": "Claude 4.5 Opus - Advanced reasoning, multimodal",
+ "input_cost": 0.000005,
+ "output_cost": 0.000025,
+ "inference_profile_geos": ["us", "eu", "global"]
},
{
- "label": "anthropic.claude-v2",
- "name": "anthropic.claude-v2",
- "description": "Text generation, conversation, complex reasoning and analysis",
- "input_cost": 0.000008,
- "output_cost": 0.000024
+ "label": "anthropic.claude-opus-4-7",
+ "name": "anthropic.claude-opus-4-7",
+ "description": "Claude 4.7 Opus - Latest and most powerful Claude, multimodal",
+ "input_cost": 0.000005,
+ "output_cost": 0.000025,
+ "inference_profile_geos": ["us", "eu", "jp", "global"]
},
{
- "label": "meta.llama2-13b-chat-v1",
- "name": "meta.llama2-13b-chat-v1",
- "description": "Text generation, conversation",
- "input_cost": 0.0003,
- "output_cost": 0.0006
+ "label": "anthropic.claude-opus-4-6-v1",
+ "name": "anthropic.claude-opus-4-6-v1",
+ "description": "Claude 4.6 Opus - Most powerful Claude, multimodal",
+ "input_cost": 0.000005,
+ "output_cost": 0.000025,
+ "inference_profile_geos": ["us", "eu", "au", "global"]
},
{
- "label": "meta.llama2-70b-chat-v1",
- "name": "meta.llama2-70b-chat-v1",
- "description": "Text generation, conversation",
- "input_cost": 0.0003,
- "output_cost": 0.0006
+ "label": "deepseek.v3.2",
+ "name": "deepseek.v3.2",
+ "description": "DeepSeek V3.2 - Text generation",
+ "input_cost": 0.00000062,
+ "output_cost": 0.00000185,
+ "stop_sequences": false
+ },
+ {
+ "label": "deepseek.r1-v1:0",
+ "name": "deepseek.r1-v1:0",
+ "description": "DeepSeek-R1 - Reasoning model",
+ "input_cost": 0.00000055,
+ "output_cost": 0.0000022,
+ "inference_profile_geos": ["us"],
+ "stop_sequences": false
+ },
+ {
+ "label": "google.gemma-3-4b-it",
+ "name": "google.gemma-3-4b-it",
+ "description": "Google Gemma 3 4B - Multimodal (text, image)",
+ "input_cost": 0.00000004,
+ "output_cost": 0.00000008
+ },
+ {
+ "label": "google.gemma-3-12b-it",
+ "name": "google.gemma-3-12b-it",
+ "description": "Google Gemma 3 12B - Multimodal (text, image)",
+ "input_cost": 0.00000009,
+ "output_cost": 0.00000029
+ },
+ {
+ "label": "google.gemma-3-27b-it",
+ "name": "google.gemma-3-27b-it",
+ "description": "Google Gemma 3 27B - Multimodal (text, image)",
+ "input_cost": 0.00000023,
+ "output_cost": 0.00000038
},
{
"label": "meta.llama3-8b-instruct-v1:0",
"name": "meta.llama3-8b-instruct-v1:0",
- "description": "Text summarization, text classification, sentiment analysis",
- "input_cost": 0.0003,
- "output_cost": 0.0006
+ "description": "Meta Llama 3 8B - Text generation, classification",
+ "input_cost": 0.0000003,
+ "output_cost": 0.0000006
},
{
"label": "meta.llama3-70b-instruct-v1:0",
"name": "meta.llama3-70b-instruct-v1:0",
- "description": "Language modeling, dialog systems, code generation, text summarization, text classification, sentiment analysis",
- "input_cost": 0.00195,
- "output_cost": 0.00256
+ "description": "Meta Llama 3 70B - Advanced text generation, code generation",
+ "input_cost": 0.00000265,
+ "output_cost": 0.0000035
+ },
+ {
+ "label": "meta.llama3-1-8b-instruct-v1:0",
+ "name": "meta.llama3-1-8b-instruct-v1:0",
+ "description": "Meta Llama 3.1 8B - Text generation",
+ "input_cost": 0.0000003,
+ "output_cost": 0.0000006,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "meta.llama3-1-70b-instruct-v1:0",
+ "name": "meta.llama3-1-70b-instruct-v1:0",
+ "description": "Meta Llama 3.1 70B - Advanced text generation",
+ "input_cost": 0.00000265,
+ "output_cost": 0.0000035,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "meta.llama3-3-70b-instruct-v1:0",
+ "name": "meta.llama3-3-70b-instruct-v1:0",
+ "description": "Meta Llama 3.3 70B - Enhanced text generation",
+ "input_cost": 0.00000265,
+ "output_cost": 0.0000035,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "meta.llama4-scout-17b-instruct-v1:0",
+ "name": "meta.llama4-scout-17b-instruct-v1:0",
+ "description": "Meta Llama 4 Scout 17B - Multimodal (text, image)",
+ "input_cost": 0.0000004,
+ "output_cost": 0.0000012,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "meta.llama4-maverick-17b-instruct-v1:0",
+ "name": "meta.llama4-maverick-17b-instruct-v1:0",
+ "description": "Meta Llama 4 Maverick 17B - Multimodal (text, image)",
+ "input_cost": 0.0000004,
+ "output_cost": 0.0000012,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "minimax.minimax-m2",
+ "name": "minimax.minimax-m2",
+ "description": "MiniMax M2 - Text generation",
+ "input_cost": 0.0000003,
+ "output_cost": 0.0000012
+ },
+ {
+ "label": "minimax.minimax-m2.1",
+ "name": "minimax.minimax-m2.1",
+ "description": "MiniMax M2.1 - Enhanced text generation",
+ "input_cost": 0.0000003,
+ "output_cost": 0.0000012
},
{
"label": "mistral.mistral-7b-instruct-v0:2",
"name": "mistral.mistral-7b-instruct-v0:2",
- "description": "Classification, text generation, code generation",
- "input_cost": 0.002,
- "output_cost": 0.006
+ "description": "Mistral 7B - Classification, text generation, code generation",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000002
},
{
"label": "mistral.mixtral-8x7b-instruct-v0:1",
"name": "mistral.mixtral-8x7b-instruct-v0:1",
- "description": "Complex reasoning and analysis, text generation, code generation",
- "input_cost": 0.002,
- "output_cost": 0.006
+ "description": "Mistral Mixtral 8x7B - Complex reasoning, code generation",
+ "input_cost": 0.00000045,
+ "output_cost": 0.0000007
},
{
"label": "mistral.mistral-large-2402-v1:0",
"name": "mistral.mistral-large-2402-v1:0",
- "description": "Complex reasoning and analysis, text generation, code generation, RAG, agents",
- "input_cost": 0.002,
- "output_cost": 0.006
+ "description": "Mistral Large (24.02) - Complex reasoning, RAG, agents",
+ "input_cost": 0.000008,
+ "output_cost": 0.000024
+ },
+ {
+ "label": "mistral.mistral-small-2402-v1:0",
+ "name": "mistral.mistral-small-2402-v1:0",
+ "description": "Mistral Small (24.02) - Cost-effective text generation",
+ "input_cost": 0.000001,
+ "output_cost": 0.000003
+ },
+ {
+ "label": "mistral.ministral-3-3b-instruct",
+ "name": "mistral.ministral-3-3b-instruct",
+ "description": "Mistral Ministral 3B - Multimodal (text, image), compact",
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000001
+ },
+ {
+ "label": "mistral.ministral-3-8b-instruct",
+ "name": "mistral.ministral-3-8b-instruct",
+ "description": "Mistral Ministral 8B - Multimodal (text, image)",
+ "input_cost": 0.00000015,
+ "output_cost": 0.00000015
+ },
+ {
+ "label": "mistral.ministral-3-14b-instruct",
+ "name": "mistral.ministral-3-14b-instruct",
+ "description": "Mistral Ministral 14B - Multimodal (text, image)",
+ "input_cost": 0.0000002,
+ "output_cost": 0.0000002
+ },
+ {
+ "label": "mistral.mistral-large-3-675b-instruct",
+ "name": "mistral.mistral-large-3-675b-instruct",
+ "description": "Mistral Large 3 - Multimodal (text, image), most powerful",
+ "input_cost": 0.0000005,
+ "output_cost": 0.0000015
+ },
+ {
+ "label": "mistral.magistral-small-2509",
+ "name": "mistral.magistral-small-2509",
+ "description": "Mistral Magistral Small - Multimodal (text, image)",
+ "input_cost": 0.0000005,
+ "output_cost": 0.0000015
+ },
+ {
+ "label": "mistral.devstral-2-123b",
+ "name": "mistral.devstral-2-123b",
+ "description": "Mistral Devstral 2 123B - Code generation specialist",
+ "input_cost": 0.0000004,
+ "output_cost": 0.000002
+ },
+ {
+ "label": "mistral.pixtral-large-2502-v1:0",
+ "name": "mistral.pixtral-large-2502-v1:0",
+ "description": "Mistral Pixtral Large - Multimodal (text, image)",
+ "input_cost": 0.000003,
+ "output_cost": 0.000009,
+ "inference_profile_geos": ["us", "eu"]
+ },
+ {
+ "label": "mistral.voxtral-mini-3b-2507",
+ "name": "mistral.voxtral-mini-3b-2507",
+ "description": "Mistral Voxtral Mini - Speech and text",
+ "input_cost": 0.00000004,
+ "output_cost": 0.00000004
+ },
+ {
+ "label": "mistral.voxtral-small-24b-2507",
+ "name": "mistral.voxtral-small-24b-2507",
+ "description": "Mistral Voxtral Small - Speech and text",
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000003
+ },
+ {
+ "label": "moonshot.kimi-k2-thinking",
+ "name": "moonshot.kimi-k2-thinking",
+ "description": "Moonshot Kimi K2 Thinking - Reasoning model",
+ "input_cost": 0.0000006,
+ "output_cost": 0.0000025
+ },
+ {
+ "label": "moonshotai.kimi-k2.5",
+ "name": "moonshotai.kimi-k2.5",
+ "description": "Moonshot Kimi K2.5 - Multimodal (text, image)",
+ "input_cost": 0.0000006,
+ "output_cost": 0.000003
+ },
+ {
+ "label": "nvidia.nemotron-nano-9b-v2",
+ "name": "nvidia.nemotron-nano-9b-v2",
+ "description": "NVIDIA Nemotron Nano 9B v2 - Text generation",
+ "input_cost": 0.00000006,
+ "output_cost": 0.00000023
+ },
+ {
+ "label": "nvidia.nemotron-nano-12b-v2",
+ "name": "nvidia.nemotron-nano-12b-v2",
+ "description": "NVIDIA Nemotron Nano 12B v2 VL - Multimodal (text, image)",
+ "input_cost": 0.0000002,
+ "output_cost": 0.0000006
+ },
+ {
+ "label": "nvidia.nemotron-nano-3-30b",
+ "name": "nvidia.nemotron-nano-3-30b",
+ "description": "NVIDIA Nemotron 3 Nano 30B - Text generation",
+ "input_cost": 0.00000006,
+ "output_cost": 0.00000024
+ },
+ {
+ "label": "openai.gpt-oss-20b-1:0",
+ "name": "openai.gpt-oss-20b-1:0",
+ "description": "OpenAI GPT OSS 20B - Text generation",
+ "input_cost": 0.00000007,
+ "output_cost": 0.0000003,
+ "stop_sequences": false
+ },
+ {
+ "label": "openai.gpt-oss-120b-1:0",
+ "name": "openai.gpt-oss-120b-1:0",
+ "description": "OpenAI GPT OSS 120B - Advanced text generation",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006,
+ "stop_sequences": false
+ },
+ {
+ "label": "openai.gpt-oss-safeguard-20b",
+ "name": "openai.gpt-oss-safeguard-20b",
+ "description": "OpenAI GPT OSS Safeguard 20B - Content moderation",
+ "input_cost": 0.00000007,
+ "output_cost": 0.0000002,
+ "stop_sequences": false
+ },
+ {
+ "label": "openai.gpt-oss-safeguard-120b",
+ "name": "openai.gpt-oss-safeguard-120b",
+ "description": "OpenAI GPT OSS Safeguard 120B - Advanced content moderation",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006,
+ "stop_sequences": false
+ },
+ {
+ "label": "qwen.qwen3-32b-v1:0",
+ "name": "qwen.qwen3-32b-v1:0",
+ "description": "Qwen 3 32B - Text generation",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
+ },
+ {
+ "label": "qwen.qwen3-coder-30b-a3b-v1:0",
+ "name": "qwen.qwen3-coder-30b-a3b-v1:0",
+ "description": "Qwen 3 Coder 30B - Code generation specialist",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
+ },
+ {
+ "label": "qwen.qwen3-next-80b-a3b",
+ "name": "qwen.qwen3-next-80b-a3b",
+ "description": "Qwen 3 Next 80B - Text generation",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000012
+ },
+ {
+ "label": "qwen.qwen3-vl-235b-a22b",
+ "name": "qwen.qwen3-vl-235b-a22b",
+ "description": "Qwen 3 VL 235B - Multimodal (text, image)",
+ "input_cost": 0.00000053,
+ "output_cost": 0.00000266
+ },
+ {
+ "label": "qwen.qwen3-coder-next",
+ "name": "qwen.qwen3-coder-next",
+ "description": "Qwen 3 Coder Next - Latest code generation",
+ "input_cost": 0.0000005,
+ "output_cost": 0.0000012
+ },
+ {
+ "label": "writer.palmyra-x4-v1:0",
+ "name": "writer.palmyra-x4-v1:0",
+ "description": "Writer Palmyra X4 - Text generation",
+ "input_cost": 0.0000025,
+ "output_cost": 0.00001,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "writer.palmyra-x5-v1:0",
+ "name": "writer.palmyra-x5-v1:0",
+ "description": "Writer Palmyra X5 - Enhanced text generation",
+ "input_cost": 0.0000006,
+ "output_cost": 0.000006,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "writer.palmyra-vision-7b",
+ "name": "writer.palmyra-vision-7b",
+ "description": "Writer Palmyra Vision 7B - Multimodal (text, image)",
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
+ },
+ {
+ "label": "zai.glm-4.7",
+ "name": "zai.glm-4.7",
+ "description": "Z.AI GLM 4.7 - Text generation",
+ "input_cost": 0.0000006,
+ "output_cost": 0.0000022
+ },
+ {
+ "label": "zai.glm-4.7-flash",
+ "name": "zai.glm-4.7-flash",
+ "description": "Z.AI GLM 4.7 Flash - Fast text generation",
+ "input_cost": 0.00000007,
+ "output_cost": 0.0000004
+ },
+ {
+ "label": "minimax.minimax-m2.5",
+ "name": "minimax.minimax-m2.5",
+ "description": "MiniMax M2.5 - General purpose text generation",
+ "input_cost": 0.0000003,
+ "output_cost": 0.0000012
+ },
+ {
+ "label": "nvidia.nemotron-super-3-120b",
+ "name": "nvidia.nemotron-super-3-120b",
+ "description": "NVIDIA Nemotron 3 Super 120B A12B - Large-scale text generation",
+ "input_cost": 0.00000015,
+ "output_cost": 0.00000065
+ },
+ {
+ "label": "zai.glm-5",
+ "name": "zai.glm-5",
+ "description": "Z.AI GLM 5 - Advanced text generation",
+ "input_cost": 0.000001,
+ "output_cost": 0.0000032
+ },
+ {
+ "label": "amazon.nova-premier-v1:0 (Legacy)",
+ "name": "amazon.nova-premier-v1:0",
+ "description": "Amazon Nova Premier - Legacy",
+ "input_cost": 0.0000025,
+ "output_cost": 0.0000125,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "anthropic.claude-3-haiku-20240307-v1:0 (Legacy)",
+ "name": "anthropic.claude-3-haiku-20240307-v1:0",
+ "description": "Claude 3 Haiku - Legacy",
+ "input_cost": 0.00000025,
+ "output_cost": 0.00000125,
+ "inference_profile_geos": ["us", "eu", "apac"]
+ },
+ {
+ "label": "anthropic.claude-3-5-haiku-20241022-v1:0 (Legacy)",
+ "name": "anthropic.claude-3-5-haiku-20241022-v1:0",
+ "description": "Claude 3.5 Haiku - Legacy",
+ "input_cost": 0.0000008,
+ "output_cost": 0.000004,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "cohere.command-r-v1:0 (Legacy)",
+ "name": "cohere.command-r-v1:0",
+ "description": "Cohere Command R - Legacy",
+ "input_cost": 0.0000005,
+ "output_cost": 0.0000015
+ },
+ {
+ "label": "cohere.command-r-plus-v1:0 (Legacy)",
+ "name": "cohere.command-r-plus-v1:0",
+ "description": "Cohere Command R+ - Legacy",
+ "input_cost": 0.000003,
+ "output_cost": 0.000015
+ },
+ {
+ "label": "meta.llama3-2-1b-instruct-v1:0 (Legacy)",
+ "name": "meta.llama3-2-1b-instruct-v1:0",
+ "description": "Meta Llama 3.2 1B Instruct - Legacy",
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000001,
+ "inference_profile_geos": ["us", "eu"]
+ },
+ {
+ "label": "meta.llama3-2-3b-instruct-v1:0 (Legacy)",
+ "name": "meta.llama3-2-3b-instruct-v1:0",
+ "description": "Meta Llama 3.2 3B Instruct - Legacy",
+ "input_cost": 0.00000015,
+ "output_cost": 0.00000015,
+ "inference_profile_geos": ["us", "eu"]
+ },
+ {
+ "label": "meta.llama3-2-11b-instruct-v1:0 (Legacy)",
+ "name": "meta.llama3-2-11b-instruct-v1:0",
+ "description": "Meta Llama 3.2 11B Instruct - Legacy",
+ "input_cost": 0.00000016,
+ "output_cost": 0.00000016,
+ "inference_profile_geos": ["us"]
+ },
+ {
+ "label": "meta.llama3-2-90b-instruct-v1:0 (Legacy)",
+ "name": "meta.llama3-2-90b-instruct-v1:0",
+ "description": "Meta Llama 3.2 90B Instruct - Legacy",
+ "input_cost": 0.00000072,
+ "output_cost": 0.00000072,
+ "inference_profile_geos": ["us"]
}
],
"regions": [
@@ -206,6 +551,10 @@
"label": "ap-east-1",
"name": "ap-east-1"
},
+ {
+ "label": "ap-east-2",
+ "name": "ap-east-2"
+ },
{
"label": "ap-northeast-1",
"name": "ap-northeast-1"
@@ -250,6 +599,10 @@
"label": "ap-southeast-6",
"name": "ap-southeast-6"
},
+ {
+ "label": "ap-southeast-7",
+ "name": "ap-southeast-7"
+ },
{
"label": "ca-central-1",
"name": "ca-central-1"
@@ -310,6 +663,10 @@
"label": "me-south-1",
"name": "me-south-1"
},
+ {
+ "label": "mx-central-1",
+ "name": "mx-central-1"
+ },
{
"label": "sa-east-1",
"name": "sa-east-1"
@@ -394,38 +751,38 @@
{
"label": "gpt-4.1",
"name": "gpt-4.1",
- "input_cost": 2e-6,
- "output_cost": 8e-6
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
},
{
"label": "o3-mini",
"name": "o3-mini",
- "input_cost": 1.1e-6,
- "output_cost": 4.4e-6
+ "input_cost": 0.0000011,
+ "output_cost": 0.0000044
},
{
"label": "o3",
"name": "o3",
- "input_cost": 2e-6,
- "output_cost": 8e-6
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
},
{
"label": "o3-pro",
"name": "o3-pro",
- "input_cost": 2e-5,
- "output_cost": 8e-5
+ "input_cost": 0.00002,
+ "output_cost": 0.00008
},
{
"label": "o4-mini",
"name": "o4-mini",
- "input_cost": 1.1e-6,
- "output_cost": 4.4e-6
+ "input_cost": 0.0000011,
+ "output_cost": 0.0000044
},
{
"label": "codex-mini",
"name": "codex-mini",
- "input_cost": 1.5e-6,
- "output_cost": 6e-6
+ "input_cost": 0.0000015,
+ "output_cost": 0.000006
},
{
"label": "o1",
@@ -442,19 +799,19 @@
{
"label": "o1-mini",
"name": "o1-mini",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000012
},
{
"label": "gpt-4o-mini",
"name": "gpt-4o-mini",
- "input_cost": 1.5e-7,
- "output_cost": 6e-7
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
},
{
"label": "gpt-4o",
"name": "gpt-4o",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
@@ -472,14 +829,14 @@
{
"label": "gpt-35-turbo",
"name": "gpt-35-turbo",
- "input_cost": 1.5e-6,
- "output_cost": 2e-6
+ "input_cost": 0.0000015,
+ "output_cost": 0.000002
},
{
"label": "gpt-35-turbo-16k",
"name": "gpt-35-turbo-16k",
- "input_cost": 3e-6,
- "output_cost": 4e-6
+ "input_cost": 0.000003,
+ "output_cost": 0.000004
},
{
"label": "gpt-4-vision-preview",
@@ -502,14 +859,14 @@
{
"label": "gpt-4.1-nano",
"name": "gpt-4.1-nano",
- "input_cost": 1e-7,
- "output_cost": 4e-7
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000004
},
{
"label": "gpt-5-pro",
"name": "gpt-5-pro",
- "input_cost": 2.1e-5,
- "output_cost": 1.68e-4
+ "input_cost": 0.000021,
+ "output_cost": 0.000168
},
{
"label": "gpt-5-chat-latest",
@@ -525,13 +882,13 @@
{
"label": "gpt-4o-mini",
"name": "gpt-4o-mini",
- "input_cost": 1.5e-7,
- "output_cost": 6e-7
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
},
{
"label": "gpt-4o",
"name": "gpt-4o",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
@@ -555,13 +912,13 @@
{
"label": "gpt-35-turbo",
"name": "gpt-35-turbo",
- "input_cost": 1.5e-6,
- "output_cost": 2e-6
+ "input_cost": 0.0000015,
+ "output_cost": 0.000002
},
{
"label": "gpt-35-turbo-16k",
"name": "gpt-35-turbo-16k",
- "input_cost": 5e-7,
+ "input_cost": 0.0000005,
"output_cost": 0.0000015
},
{
@@ -660,21 +1017,21 @@
"label": "claude-3-7-sonnet-latest",
"name": "claude-3-7-sonnet-latest",
"description": "Most recent snapshot version of Claude Sonnet 3.7",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000015
},
{
"label": "claude-3-5-haiku-latest",
"name": "claude-3-5-haiku-latest",
"description": "Most recent snapshot version of Claude Haiku 3.5",
- "input_cost": 8e-7,
- "output_cost": 4e-6
+ "input_cost": 0.0000008,
+ "output_cost": 0.000004
},
{
"label": "claude-3.5-sonnet-latest",
"name": "claude-3-5-sonnet-latest",
"description": "Most recent snapshot version of Claude Sonnet 3.5 model",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000015
},
{
@@ -695,8 +1052,8 @@
"label": "claude-3-haiku",
"name": "claude-3-haiku-20240307",
"description": "Fastest and most compact model, designed for near-instant responsiveness",
- "input_cost": 2.5e-7,
- "output_cost": 1.25e-6
+ "input_cost": 0.00000025,
+ "output_cost": 0.00000125
}
]
},
@@ -707,8 +1064,8 @@
"label": "claude-3-haiku",
"name": "claude-3-haiku",
"description": "Fastest and most compact model, designed for near-instant responsiveness",
- "input_cost": 2.5e-7,
- "output_cost": 1.25e-6
+ "input_cost": 0.00000025,
+ "output_cost": 0.00000125
},
{
"label": "claude-3-opus",
@@ -776,56 +1133,56 @@
{
"label": "gemini-2.5-pro",
"name": "gemini-2.5-pro",
- "input_cost": 0.3e-6,
+ "input_cost": 0.0000003,
"output_cost": 0.000025
},
{
"label": "gemini-2.5-flash",
"name": "gemini-2.5-flash",
- "input_cost": 1.25e-6,
+ "input_cost": 0.00000125,
"output_cost": 0.00001
},
{
"label": "gemini-2.5-flash-image",
"name": "gemini-2.5-flash-image",
- "input_cost": 1.25e-6,
+ "input_cost": 0.00000125,
"output_cost": 0.00001
},
{
"label": "gemini-2.5-flash-lite",
"name": "gemini-2.5-flash-lite",
- "input_cost": 1e-7,
- "output_cost": 4e-7
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000004
},
{
"label": "gemini-2.0-flash",
"name": "gemini-2.0-flash",
- "input_cost": 1e-7,
- "output_cost": 4e-7
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000004
},
{
"label": "gemini-2.0-flash-lite",
"name": "gemini-2.0-flash-lite",
- "input_cost": 7.5e-8,
- "output_cost": 3e-7
+ "input_cost": 0.000000075,
+ "output_cost": 0.0000003
},
{
"label": "gemini-1.5-flash",
"name": "gemini-1.5-flash",
- "input_cost": 7.5e-8,
- "output_cost": 3e-7
+ "input_cost": 0.000000075,
+ "output_cost": 0.0000003
},
{
"label": "gemini-1.5-flash-8b",
"name": "gemini-1.5-flash-8b",
- "input_cost": 3.75e-8,
- "output_cost": 1.5e-7
+ "input_cost": 0.0000000375,
+ "output_cost": 0.00000015
},
{
"label": "gemini-1.5-pro",
"name": "gemini-1.5-pro",
- "input_cost": 1.25e-6,
- "output_cost": 5e-6
+ "input_cost": 0.00000125,
+ "output_cost": 0.000005
}
]
},
@@ -885,68 +1242,68 @@
{
"label": "gemini-2.5-pro",
"name": "gemini-2.5-pro",
- "input_cost": 0.3e-6,
+ "input_cost": 0.0000003,
"output_cost": 0.000025
},
{
"label": "gemini-2.5-flash",
"name": "gemini-2.5-flash",
- "input_cost": 1.25e-6,
+ "input_cost": 0.00000125,
"output_cost": 0.00001
},
{
"label": "gemini-2.5-flash-lite",
"name": "gemini-2.5-flash-lite",
- "input_cost": 1e-7,
- "output_cost": 4e-7
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000004
},
{
"label": "gemini-2.0-flash",
"name": "gemini-2.0-flash-001",
- "input_cost": 1e-7,
- "output_cost": 4e-7
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000004
},
{
"label": "gemini-2.0-flash-lite",
"name": "gemini-2.0-flash-lite-001",
- "input_cost": 7.5e-8,
- "output_cost": 3e-7
+ "input_cost": 0.000000075,
+ "output_cost": 0.0000003
},
{
"label": "gemini-1.5-flash-002",
"name": "gemini-1.5-flash-002",
- "input_cost": 7.5e-8,
- "output_cost": 3e-7
+ "input_cost": 0.000000075,
+ "output_cost": 0.0000003
},
{
"label": "gemini-1.5-flash-001",
"name": "gemini-1.5-flash-001",
- "input_cost": 7.5e-8,
- "output_cost": 3e-7
+ "input_cost": 0.000000075,
+ "output_cost": 0.0000003
},
{
"label": "gemini-1.5-pro-002",
"name": "gemini-1.5-pro-002",
- "input_cost": 1.25e-6,
- "output_cost": 5e-6
+ "input_cost": 0.00000125,
+ "output_cost": 0.000005
},
{
"label": "gemini-1.5-pro-001",
"name": "gemini-1.5-pro-001",
- "input_cost": 1.25e-6,
- "output_cost": 5e-6
+ "input_cost": 0.00000125,
+ "output_cost": 0.000005
},
{
"label": "gemini-1.0-pro",
"name": "gemini-1.0-pro",
- "input_cost": 1.25e-7,
- "output_cost": 3.75e-7
+ "input_cost": 0.000000125,
+ "output_cost": 0.000000375
},
{
"label": "gemini-1.0-pro-vision",
"name": "gemini-1.0-pro-vision",
- "input_cost": 1.25e-7,
- "output_cost": 3.75e-7
+ "input_cost": 0.000000125,
+ "output_cost": 0.000000375
},
{
"label": "claude-opus-4-7",
@@ -1015,21 +1372,21 @@
"label": "claude-3-7-sonnet@20250219",
"name": "claude-3-7-sonnet@20250219",
"description": "(20250219-v1:0) specific version of Claude Sonnet 3.7",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000015
},
{
"label": "claude-3-5-haiku@20241022",
"name": "claude-3-5-haiku@20241022",
"description": "(20241022-v1:0) specific version of Claude Haiku 3.5",
- "input_cost": 8e-7,
- "output_cost": 4e-6
+ "input_cost": 0.0000008,
+ "output_cost": 0.000004
},
{
"label": "claude-3-5-sonnet-v2@20241022",
"name": "claude-3-5-sonnet-v2@20241022",
"description": "(20241022-v2:0) specific version of Claude Sonnet 3.5",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000015
},
{
@@ -1050,54 +1407,183 @@
"label": "claude-3-haiku@20240307",
"name": "claude-3-haiku@20240307",
"description": "Fastest and most compact model for near-instant responsiveness",
- "input_cost": 2.5e-7,
- "output_cost": 1.25e-6
+ "input_cost": 0.00000025,
+ "output_cost": 0.00000125
}
],
"regions": [
- { "label": "global", "name": "global" },
- { "label": "us-east1", "name": "us-east1" },
- { "label": "us-east4", "name": "us-east4" },
- { "label": "us-central1", "name": "us-central1" },
- { "label": "us-west1", "name": "us-west1" },
- { "label": "europe-west4", "name": "europe-west4" },
- { "label": "europe-west1", "name": "europe-west1" },
- { "label": "europe-west3", "name": "europe-west3" },
- { "label": "europe-west2", "name": "europe-west2" },
- { "label": "asia-east1", "name": "asia-east1" },
- { "label": "asia-southeast1", "name": "asia-southeast1" },
- { "label": "asia-northeast1", "name": "asia-northeast1" },
- { "label": "asia-south1", "name": "asia-south1" },
- { "label": "australia-southeast1", "name": "australia-southeast1" },
- { "label": "southamerica-east1", "name": "southamerica-east1" },
- { "label": "africa-south1", "name": "africa-south1" },
- { "label": "asia-east2", "name": "asia-east2" },
- { "label": "asia-northeast2", "name": "asia-northeast2" },
- { "label": "asia-northeast3", "name": "asia-northeast3" },
- { "label": "asia-south2", "name": "asia-south2" },
- { "label": "asia-southeast2", "name": "asia-southeast2" },
- { "label": "australia-southeast2", "name": "australia-southeast2" },
- { "label": "europe-central2", "name": "europe-central2" },
- { "label": "europe-north1", "name": "europe-north1" },
- { "label": "europe-north2", "name": "europe-north2" },
- { "label": "europe-southwest1", "name": "europe-southwest1" },
- { "label": "europe-west10", "name": "europe-west10" },
- { "label": "europe-west12", "name": "europe-west12" },
- { "label": "europe-west6", "name": "europe-west6" },
- { "label": "europe-west8", "name": "europe-west8" },
- { "label": "europe-west9", "name": "europe-west9" },
- { "label": "me-central1", "name": "me-central1" },
- { "label": "me-central2", "name": "me-central2" },
- { "label": "me-west1", "name": "me-west1" },
- { "label": "northamerica-northeast1", "name": "northamerica-northeast1" },
- { "label": "northamerica-northeast2", "name": "northamerica-northeast2" },
- { "label": "northamerica-south1", "name": "northamerica-south1" },
- { "label": "southamerica-west1", "name": "southamerica-west1" },
- { "label": "us-east5", "name": "us-east5" },
- { "label": "us-south1", "name": "us-south1" },
- { "label": "us-west2", "name": "us-west2" },
- { "label": "us-west3", "name": "us-west3" },
- { "label": "us-west4", "name": "us-west4" }
+ {
+ "label": "global",
+ "name": "global"
+ },
+ {
+ "label": "us-east1",
+ "name": "us-east1"
+ },
+ {
+ "label": "us-east4",
+ "name": "us-east4"
+ },
+ {
+ "label": "us-central1",
+ "name": "us-central1"
+ },
+ {
+ "label": "us-west1",
+ "name": "us-west1"
+ },
+ {
+ "label": "europe-west4",
+ "name": "europe-west4"
+ },
+ {
+ "label": "europe-west1",
+ "name": "europe-west1"
+ },
+ {
+ "label": "europe-west3",
+ "name": "europe-west3"
+ },
+ {
+ "label": "europe-west2",
+ "name": "europe-west2"
+ },
+ {
+ "label": "asia-east1",
+ "name": "asia-east1"
+ },
+ {
+ "label": "asia-southeast1",
+ "name": "asia-southeast1"
+ },
+ {
+ "label": "asia-northeast1",
+ "name": "asia-northeast1"
+ },
+ {
+ "label": "asia-south1",
+ "name": "asia-south1"
+ },
+ {
+ "label": "australia-southeast1",
+ "name": "australia-southeast1"
+ },
+ {
+ "label": "southamerica-east1",
+ "name": "southamerica-east1"
+ },
+ {
+ "label": "africa-south1",
+ "name": "africa-south1"
+ },
+ {
+ "label": "asia-east2",
+ "name": "asia-east2"
+ },
+ {
+ "label": "asia-northeast2",
+ "name": "asia-northeast2"
+ },
+ {
+ "label": "asia-northeast3",
+ "name": "asia-northeast3"
+ },
+ {
+ "label": "asia-south2",
+ "name": "asia-south2"
+ },
+ {
+ "label": "asia-southeast2",
+ "name": "asia-southeast2"
+ },
+ {
+ "label": "australia-southeast2",
+ "name": "australia-southeast2"
+ },
+ {
+ "label": "europe-central2",
+ "name": "europe-central2"
+ },
+ {
+ "label": "europe-north1",
+ "name": "europe-north1"
+ },
+ {
+ "label": "europe-north2",
+ "name": "europe-north2"
+ },
+ {
+ "label": "europe-southwest1",
+ "name": "europe-southwest1"
+ },
+ {
+ "label": "europe-west10",
+ "name": "europe-west10"
+ },
+ {
+ "label": "europe-west12",
+ "name": "europe-west12"
+ },
+ {
+ "label": "europe-west6",
+ "name": "europe-west6"
+ },
+ {
+ "label": "europe-west8",
+ "name": "europe-west8"
+ },
+ {
+ "label": "europe-west9",
+ "name": "europe-west9"
+ },
+ {
+ "label": "me-central1",
+ "name": "me-central1"
+ },
+ {
+ "label": "me-central2",
+ "name": "me-central2"
+ },
+ {
+ "label": "me-west1",
+ "name": "me-west1"
+ },
+ {
+ "label": "northamerica-northeast1",
+ "name": "northamerica-northeast1"
+ },
+ {
+ "label": "northamerica-northeast2",
+ "name": "northamerica-northeast2"
+ },
+ {
+ "label": "northamerica-south1",
+ "name": "northamerica-south1"
+ },
+ {
+ "label": "southamerica-west1",
+ "name": "southamerica-west1"
+ },
+ {
+ "label": "us-east5",
+ "name": "us-east5"
+ },
+ {
+ "label": "us-south1",
+ "name": "us-south1"
+ },
+ {
+ "label": "us-west2",
+ "name": "us-west2"
+ },
+ {
+ "label": "us-west3",
+ "name": "us-west3"
+ },
+ {
+ "label": "us-west4",
+ "name": "us-west4"
+ }
]
},
{
@@ -1362,20 +1848,20 @@
{
"label": "gpt-4.1",
"name": "gpt-4.1",
- "input_cost": 2e-6,
- "output_cost": 8e-6
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
},
{
"label": "gpt-4.1-mini",
"name": "gpt-4.1-mini",
- "input_cost": 4e-7,
- "output_cost": 1.6e-6
+ "input_cost": 0.0000004,
+ "output_cost": 0.0000016
},
{
"label": "gpt-4.1-nano",
"name": "gpt-4.1-nano",
- "input_cost": 1e-7,
- "output_cost": 4e-7
+ "input_cost": 0.0000001,
+ "output_cost": 0.0000004
},
{
"label": "gpt-4.5-preview",
@@ -1386,56 +1872,56 @@
{
"label": "gpt-4o-mini (latest)",
"name": "gpt-4o-mini",
- "input_cost": 1.5e-7,
- "output_cost": 6e-7
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
},
{
"label": "gpt-4o-mini-2024-07-18",
"name": "gpt-4o-mini-2024-07-18",
- "input_cost": 1.5e-7,
- "output_cost": 6e-7
+ "input_cost": 0.00000015,
+ "output_cost": 0.0000006
},
{
"label": "gpt-4o (latest)",
"name": "gpt-4o",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
"label": "gpt-4o-2024-11-20",
"name": "gpt-4o-2024-11-20",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
"label": "gpt-4o-2024-08-06",
"name": "gpt-4o-2024-08-06",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
"label": "gpt-4o-2024-05-13",
"name": "gpt-4o-2024-05-13",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
"label": "o4-mini (latest)",
"name": "o4-mini",
- "input_cost": 1.1e-6,
- "output_cost": 4.4e-6
+ "input_cost": 0.0000011,
+ "output_cost": 0.0000044
},
{
"label": "o3-mini (latest)",
"name": "o3-mini",
- "input_cost": 1.1e-6,
- "output_cost": 4.4e-6
+ "input_cost": 0.0000011,
+ "output_cost": 0.0000044
},
{
"label": "o3-mini-2025-01-31",
"name": "o3-mini-2025-01-31",
- "input_cost": 1.1e-6,
- "output_cost": 4.4e-6
+ "input_cost": 0.0000011,
+ "output_cost": 0.0000044
},
{
"label": "o1-preview (latest)",
@@ -1452,13 +1938,13 @@
{
"label": "o1-mini (latest)",
"name": "o1-mini",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000012
},
{
"label": "o1-mini-2024-09-12",
"name": "o1-mini-2024-09-12",
- "input_cost": 3e-6,
+ "input_cost": 0.000003,
"output_cost": 0.000012
},
{
@@ -1524,13 +2010,13 @@
{
"label": "gpt-3.5-turbo",
"name": "gpt-3.5-turbo",
- "input_cost": 1.5e-6,
- "output_cost": 2e-6
+ "input_cost": 0.0000015,
+ "output_cost": 0.000002
},
{
"label": "gpt-3.5-turbo-0125",
"name": "gpt-3.5-turbo-0125",
- "input_cost": 5e-7,
+ "input_cost": 0.0000005,
"output_cost": 0.0000015
},
{
@@ -1548,7 +2034,7 @@
{
"label": "gpt-3.5-turbo-16k",
"name": "gpt-3.5-turbo-16k",
- "input_cost": 5e-7,
+ "input_cost": 0.0000005,
"output_cost": 0.0000015
},
{
@@ -1577,7 +2063,7 @@
{
"label": "gpt-4o",
"name": "gpt-4o",
- "input_cost": 2.5e-6,
+ "input_cost": 0.0000025,
"output_cost": 0.00001
},
{
@@ -1637,8 +2123,8 @@
{
"label": "gpt-3.5-turbo",
"name": "gpt-3.5-turbo",
- "input_cost": 1.5e-6,
- "output_cost": 2e-6
+ "input_cost": 0.0000015,
+ "output_cost": 0.000002
},
{
"label": "gpt-3.5-turbo-1106",
@@ -1655,7 +2141,7 @@
{
"label": "gpt-3.5-turbo-16k",
"name": "gpt-3.5-turbo-16k",
- "input_cost": 5e-7,
+ "input_cost": 0.0000005,
"output_cost": 0.0000015
},
{
@@ -1672,38 +2158,38 @@
{
"label": "sonar",
"name": "sonar",
- "input_cost": 1e-6,
- "output_cost": 1e-6
+ "input_cost": 0.000001,
+ "output_cost": 0.000001
},
{
"label": "sonar-pro",
"name": "sonar-pro",
- "input_cost": 3e-6,
- "output_cost": 1.5e-5
+ "input_cost": 0.000003,
+ "output_cost": 0.000015
},
{
"label": "sonar-reasoning",
"name": "sonar-reasoning",
- "input_cost": 1e-6,
- "output_cost": 5e-6
+ "input_cost": 0.000001,
+ "output_cost": 0.000005
},
{
"label": "sonar-reasoning-pro",
"name": "sonar-reasoning-pro",
- "input_cost": 2e-6,
- "output_cost": 8e-6
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
},
{
"label": "sonar-deep-research",
"name": "sonar-deep-research",
- "input_cost": 2e-6,
- "output_cost": 8e-6
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
},
{
"label": "r1-1776",
"name": "r1-1776",
- "input_cost": 2e-6,
- "output_cost": 8e-6
+ "input_cost": 0.000002,
+ "output_cost": 0.000008
}
]
},
@@ -2091,8 +2577,8 @@
{
"label": "gpt-35-turbo",
"name": "gpt-35-turbo",
- "input_cost": 1.5e-6,
- "output_cost": 2e-6
+ "input_cost": 0.0000015,
+ "output_cost": 0.000002
},
{
"label": "gpt-4",
@@ -2155,38 +2641,38 @@
{
"label": "text-bison",
"name": "text-bison",
- "input_cost": 2.5e-7,
- "output_cost": 5e-7
+ "input_cost": 0.00000025,
+ "output_cost": 0.0000005
},
{
"label": "code-bison",
"name": "code-bison",
- "input_cost": 2.5e-7,
- "output_cost": 5e-7
+ "input_cost": 0.00000025,
+ "output_cost": 0.0000005
},
{
"label": "code-gecko",
"name": "code-gecko",
- "input_cost": 2.5e-7,
- "output_cost": 5e-7
+ "input_cost": 0.00000025,
+ "output_cost": 0.0000005
},
{
"label": "text-bison-32k",
"name": "text-bison-32k",
- "input_cost": 2.5e-7,
- "output_cost": 5e-7
+ "input_cost": 0.00000025,
+ "output_cost": 0.0000005
},
{
"label": "code-bison-32k",
"name": "code-bison-32k",
- "input_cost": 2.5e-7,
- "output_cost": 5e-7
+ "input_cost": 0.00000025,
+ "output_cost": 0.0000005
},
{
"label": "code-gecko-32k",
"name": "code-gecko-32k",
- "input_cost": 2.5e-7,
- "output_cost": 5e-7
+ "input_cost": 0.00000025,
+ "output_cost": 0.0000005
}
]
},
@@ -2202,7 +2688,7 @@
{
"label": "babbage-002",
"name": "babbage-002",
- "input_cost": 4e-7,
+ "input_cost": 0.0000004,
"output_cost": 0.0000016
},
{
@@ -2355,48 +2841,174 @@
}
],
"regions": [
- { "label": "us-east1", "name": "us-east1" },
- { "label": "us-east4", "name": "us-east4" },
- { "label": "us-central1", "name": "us-central1" },
- { "label": "us-west1", "name": "us-west1" },
- { "label": "europe-west4", "name": "europe-west4" },
- { "label": "europe-west1", "name": "europe-west1" },
- { "label": "europe-west3", "name": "europe-west3" },
- { "label": "europe-west2", "name": "europe-west2" },
- { "label": "asia-east1", "name": "asia-east1" },
- { "label": "asia-southeast1", "name": "asia-southeast1" },
- { "label": "asia-northeast1", "name": "asia-northeast1" },
- { "label": "asia-south1", "name": "asia-south1" },
- { "label": "australia-southeast1", "name": "australia-southeast1" },
- { "label": "southamerica-east1", "name": "southamerica-east1" },
- { "label": "africa-south1", "name": "africa-south1" },
- { "label": "asia-east2", "name": "asia-east2" },
- { "label": "asia-northeast2", "name": "asia-northeast2" },
- { "label": "asia-northeast3", "name": "asia-northeast3" },
- { "label": "asia-south2", "name": "asia-south2" },
- { "label": "asia-southeast2", "name": "asia-southeast2" },
- { "label": "australia-southeast2", "name": "australia-southeast2" },
- { "label": "europe-central2", "name": "europe-central2" },
- { "label": "europe-north1", "name": "europe-north1" },
- { "label": "europe-north2", "name": "europe-north2" },
- { "label": "europe-southwest1", "name": "europe-southwest1" },
- { "label": "europe-west10", "name": "europe-west10" },
- { "label": "europe-west12", "name": "europe-west12" },
- { "label": "europe-west6", "name": "europe-west6" },
- { "label": "europe-west8", "name": "europe-west8" },
- { "label": "europe-west9", "name": "europe-west9" },
- { "label": "me-central1", "name": "me-central1" },
- { "label": "me-central2", "name": "me-central2" },
- { "label": "me-west1", "name": "me-west1" },
- { "label": "northamerica-northeast1", "name": "northamerica-northeast1" },
- { "label": "northamerica-northeast2", "name": "northamerica-northeast2" },
- { "label": "northamerica-south1", "name": "northamerica-south1" },
- { "label": "southamerica-west1", "name": "southamerica-west1" },
- { "label": "us-east5", "name": "us-east5" },
- { "label": "us-south1", "name": "us-south1" },
- { "label": "us-west2", "name": "us-west2" },
- { "label": "us-west3", "name": "us-west3" },
- { "label": "us-west4", "name": "us-west4" }
+ {
+ "label": "us-east1",
+ "name": "us-east1"
+ },
+ {
+ "label": "us-east4",
+ "name": "us-east4"
+ },
+ {
+ "label": "us-central1",
+ "name": "us-central1"
+ },
+ {
+ "label": "us-west1",
+ "name": "us-west1"
+ },
+ {
+ "label": "europe-west4",
+ "name": "europe-west4"
+ },
+ {
+ "label": "europe-west1",
+ "name": "europe-west1"
+ },
+ {
+ "label": "europe-west3",
+ "name": "europe-west3"
+ },
+ {
+ "label": "europe-west2",
+ "name": "europe-west2"
+ },
+ {
+ "label": "asia-east1",
+ "name": "asia-east1"
+ },
+ {
+ "label": "asia-southeast1",
+ "name": "asia-southeast1"
+ },
+ {
+ "label": "asia-northeast1",
+ "name": "asia-northeast1"
+ },
+ {
+ "label": "asia-south1",
+ "name": "asia-south1"
+ },
+ {
+ "label": "australia-southeast1",
+ "name": "australia-southeast1"
+ },
+ {
+ "label": "southamerica-east1",
+ "name": "southamerica-east1"
+ },
+ {
+ "label": "africa-south1",
+ "name": "africa-south1"
+ },
+ {
+ "label": "asia-east2",
+ "name": "asia-east2"
+ },
+ {
+ "label": "asia-northeast2",
+ "name": "asia-northeast2"
+ },
+ {
+ "label": "asia-northeast3",
+ "name": "asia-northeast3"
+ },
+ {
+ "label": "asia-south2",
+ "name": "asia-south2"
+ },
+ {
+ "label": "asia-southeast2",
+ "name": "asia-southeast2"
+ },
+ {
+ "label": "australia-southeast2",
+ "name": "australia-southeast2"
+ },
+ {
+ "label": "europe-central2",
+ "name": "europe-central2"
+ },
+ {
+ "label": "europe-north1",
+ "name": "europe-north1"
+ },
+ {
+ "label": "europe-north2",
+ "name": "europe-north2"
+ },
+ {
+ "label": "europe-southwest1",
+ "name": "europe-southwest1"
+ },
+ {
+ "label": "europe-west10",
+ "name": "europe-west10"
+ },
+ {
+ "label": "europe-west12",
+ "name": "europe-west12"
+ },
+ {
+ "label": "europe-west6",
+ "name": "europe-west6"
+ },
+ {
+ "label": "europe-west8",
+ "name": "europe-west8"
+ },
+ {
+ "label": "europe-west9",
+ "name": "europe-west9"
+ },
+ {
+ "label": "me-central1",
+ "name": "me-central1"
+ },
+ {
+ "label": "me-central2",
+ "name": "me-central2"
+ },
+ {
+ "label": "me-west1",
+ "name": "me-west1"
+ },
+ {
+ "label": "northamerica-northeast1",
+ "name": "northamerica-northeast1"
+ },
+ {
+ "label": "northamerica-northeast2",
+ "name": "northamerica-northeast2"
+ },
+ {
+ "label": "northamerica-south1",
+ "name": "northamerica-south1"
+ },
+ {
+ "label": "southamerica-west1",
+ "name": "southamerica-west1"
+ },
+ {
+ "label": "us-east5",
+ "name": "us-east5"
+ },
+ {
+ "label": "us-south1",
+ "name": "us-south1"
+ },
+ {
+ "label": "us-west2",
+ "name": "us-west2"
+ },
+ {
+ "label": "us-west3",
+ "name": "us-west3"
+ },
+ {
+ "label": "us-west4",
+ "name": "us-west4"
+ }
]
},
{
@@ -2454,38 +3066,55 @@
{
"label": "amazon.titan-embed-text-v1",
"name": "amazon.titan-embed-text-v1",
- "description": "Embedding Dimensions: 1536"
+ "description": "Amazon Titan Text Embeddings V1 - Dimensions: 1536"
},
{
- "label": "amazon.titan-embed-text-v2",
+ "label": "amazon.titan-embed-text-v2:0",
"name": "amazon.titan-embed-text-v2:0",
- "description": "Embedding Dimensions: 1024"
+ "description": "Amazon Titan Text Embeddings V2 - Dimensions: 1024"
},
{
"label": "amazon.titan-embed-g1-text-02",
"name": "amazon.titan-embed-g1-text-02",
- "description": "Embedding Dimensions: 1536"
+ "description": "Amazon Titan Text Embeddings G1 - Dimensions: 1536"
+ },
+ {
+ "label": "amazon.titan-embed-image-v1",
+ "name": "amazon.titan-embed-image-v1",
+ "description": "Amazon Titan Multimodal Embeddings G1 - Text and Image"
+ },
+ {
+ "label": "amazon.nova-2-multimodal-embeddings-v1:0",
+ "name": "amazon.nova-2-multimodal-embeddings-v1:0",
+ "description": "Amazon Nova 2 Multimodal Embeddings - Text, Image, Audio, Video"
},
{
"label": "cohere.embed-english-v3",
"name": "cohere.embed-english-v3",
- "description": "Embedding Dimensions: 1024"
+ "description": "Cohere Embed English V3 - Dimensions: 1024"
},
{
"label": "cohere.embed-multilingual-v3",
"name": "cohere.embed-multilingual-v3",
- "description": "Embedding Dimensions: 1024"
+ "description": "Cohere Embed Multilingual V3 - Dimensions: 1024"
+ },
+ {
+ "label": "cohere.embed-v4:0",
+ "name": "cohere.embed-v4:0",
+ "description": "Cohere Embed V4 - Multimodal (text, image)"
+ },
+ {
+ "label": "twelvelabs.marengo-embed-2-7-v1:0",
+ "name": "twelvelabs.marengo-embed-2-7-v1:0",
+ "description": "TwelveLabs Marengo Embed 2.7 - Video embeddings"
+ },
+ {
+ "label": "twelvelabs.marengo-embed-3-0-v1:0",
+ "name": "twelvelabs.marengo-embed-3-0-v1:0",
+ "description": "TwelveLabs Marengo Embed 3.0 - Video embeddings"
}
],
"regions": [
- {
- "label": "af-south-1",
- "name": "af-south-1"
- },
- {
- "label": "ap-east-1",
- "name": "ap-east-1"
- },
{
"label": "ap-northeast-1",
"name": "ap-northeast-1"
@@ -2522,30 +3151,10 @@
"label": "ap-southeast-4",
"name": "ap-southeast-4"
},
- {
- "label": "ap-southeast-5",
- "name": "ap-southeast-5"
- },
- {
- "label": "ap-southeast-6",
- "name": "ap-southeast-6"
- },
{
"label": "ca-central-1",
"name": "ca-central-1"
},
- {
- "label": "ca-west-1",
- "name": "ca-west-1"
- },
- {
- "label": "cn-north-1",
- "name": "cn-north-1"
- },
- {
- "label": "cn-northwest-1",
- "name": "cn-northwest-1"
- },
{
"label": "eu-central-1",
"name": "eu-central-1"
@@ -2578,18 +3187,6 @@
"label": "eu-west-3",
"name": "eu-west-3"
},
- {
- "label": "il-central-1",
- "name": "il-central-1"
- },
- {
- "label": "me-central-1",
- "name": "me-central-1"
- },
- {
- "label": "me-south-1",
- "name": "me-south-1"
- },
{
"label": "sa-east-1",
"name": "sa-east-1"
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSBedrockCatalog.test.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSBedrockCatalog.test.ts
new file mode 100644
index 000000000..62fc2737b
--- /dev/null
+++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSBedrockCatalog.test.ts
@@ -0,0 +1,111 @@
+import * as fs from 'fs'
+import * as path from 'path'
+
+describe('AWS Bedrock model catalog (models.json)', () => {
+ let bedrockChat: any
+
+ beforeAll(() => {
+ const modelsPath = path.join(__dirname, '..', '..', '..', 'models.json')
+ const raw = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
+ bedrockChat = raw.chat.find((c: any) => c.name === 'awsChatBedrock')
+ })
+
+ it('awsChatBedrock section exists', () => {
+ expect(bedrockChat).toBeDefined()
+ })
+
+ it('has a non-empty models array', () => {
+ expect(bedrockChat.models.length).toBeGreaterThan(0)
+ })
+
+ it('has a non-empty regions array', () => {
+ expect(bedrockChat.regions.length).toBeGreaterThan(0)
+ })
+
+ it('every model has label and name', () => {
+ for (const m of bedrockChat.models) {
+ expect(m.label).toBeTruthy()
+ expect(m.name).toBeTruthy()
+ }
+ })
+
+ it('does not contain TwelveLabs Pegasus (not a chat model)', () => {
+ const pegasus = bedrockChat.models.find((m: any) => m.name.includes('twelvelabs.pegasus'))
+ expect(pegasus).toBeUndefined()
+ })
+
+ it('contains models added from AWS API', () => {
+ const names = bedrockChat.models.map((m: any) => m.name)
+ expect(names).toContain('minimax.minimax-m2.5')
+ expect(names).toContain('nvidia.nemotron-super-3-120b')
+ expect(names).toContain('zai.glm-5')
+ expect(names).toContain('writer.palmyra-vision-7b')
+ })
+
+ it('legacy models are present with (Legacy) label', () => {
+ const legacyModels = bedrockChat.models.filter((m: any) => m.label.includes('(Legacy)'))
+ expect(legacyModels.length).toBeGreaterThan(0)
+
+ const legacyNames = legacyModels.map((m: any) => m.name)
+ expect(legacyNames).toContain('anthropic.claude-3-haiku-20240307-v1:0')
+ expect(legacyNames).toContain('meta.llama3-2-1b-instruct-v1:0')
+ expect(legacyNames).toContain('cohere.command-r-v1:0')
+
+ for (const m of legacyModels) {
+ expect(m.label).toContain('(Legacy)')
+ expect(m.description).toMatch(/Legacy|deprecated/i)
+ }
+ })
+
+ it('does not contain stale/wrong model IDs', () => {
+ const names = bedrockChat.models.map((m: any) => m.name)
+ expect(names).not.toContain('deepseek.v3-v1:0')
+ expect(names).not.toContain('meta.llama3-1-405b-instruct-v1:0')
+ expect(names).not.toContain('mistral.mistral-large-2407-v1:0')
+ expect(names).not.toContain('nvidia.nemotron-3-super-120b-a12b')
+ expect(names).not.toContain('qwen.qwen3-235b-a22b-2507-v1:0')
+ expect(names).not.toContain('qwen.qwen3-coder-480b-a35b-v1:0')
+ })
+
+ it('contains Claude 4.6 Sonnet (auto-converted via code)', () => {
+ const claude46 = bedrockChat.models.find((m: any) => m.name === 'anthropic.claude-sonnet-4-6')
+ expect(claude46).toBeDefined()
+ })
+
+ it('model names do not have leading/trailing whitespace', () => {
+ for (const m of bedrockChat.models) {
+ expect(m.name).toBe(m.name.trim())
+ }
+ })
+
+ it('marks correct models with stop_sequences: false', () => {
+ const noStopSeq = bedrockChat.models.filter((m: any) => m.stop_sequences === false).map((m: any) => m.name)
+ expect(noStopSeq).toContain('deepseek.v3.2')
+ expect(noStopSeq).toContain('deepseek.r1-v1:0')
+ expect(noStopSeq).toContain('openai.gpt-oss-20b-1:0')
+ expect(noStopSeq).toContain('openai.gpt-oss-120b-1:0')
+ expect(noStopSeq).toContain('openai.gpt-oss-safeguard-20b')
+ expect(noStopSeq).toContain('openai.gpt-oss-safeguard-120b')
+ expect(noStopSeq).toHaveLength(6)
+ })
+
+ it('does not mark other models with stop_sequences: false', () => {
+ const withStopSeq = bedrockChat.models.filter((m: any) => m.stop_sequences === false)
+ const names = withStopSeq.map((m: any) => m.name)
+ expect(names).not.toContain('anthropic.claude-sonnet-4-6')
+ expect(names).not.toContain('amazon.nova-pro-v1:0')
+ })
+
+ // --- Pricing ---
+
+ it('every model has input_cost and output_cost', () => {
+ for (const m of bedrockChat.models) {
+ expect(m.input_cost).toBeDefined()
+ expect(m.output_cost).toBeDefined()
+ expect(typeof m.input_cost).toBe('number')
+ expect(typeof m.output_cost).toBe('number')
+ expect(m.input_cost).toBeGreaterThan(0)
+ expect(m.output_cost).toBeGreaterThan(0)
+ }
+ })
+})
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.test.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.test.ts
new file mode 100644
index 000000000..c0d960e08
--- /dev/null
+++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.test.ts
@@ -0,0 +1,817 @@
+import {
+ validateEndpointHost,
+ resolveBedrockModel,
+ normalizeBedrockError,
+ regionToGeoCandidates,
+ getStopSeqUnsupportedModels,
+ _resetInferenceProfileCache,
+ _resetStopSeqCache
+} from './utils'
+import { detectFormat } from './FlowiseAWSChatBedrockImported'
+
+// Mock the model loader to return models with inference_profile_geos and stop_sequences
+jest.mock('../../../src/modelLoader', () => ({
+ MODEL_TYPE: { CHAT: 'chat' },
+ getModels: jest
+ .fn()
+ .mockResolvedValue([
+ { name: 'anthropic.claude-sonnet-4-6', inference_profile_geos: ['us', 'eu', 'jp', 'au', 'global'] },
+ { name: 'anthropic.claude-opus-4-5-20251101-v1:0', inference_profile_geos: ['us', 'eu', 'global'] },
+ { name: 'amazon.nova-2-lite-v1:0', inference_profile_geos: ['us', 'eu', 'jp', 'global'] },
+ { name: 'amazon.nova-lite-v1:0', inference_profile_geos: ['us', 'eu', 'apac', 'ca'] },
+ { name: 'amazon.nova-pro-v1:0', inference_profile_geos: ['us', 'eu', 'apac'] },
+ { name: 'meta.llama4-scout-17b-instruct-v1:0', inference_profile_geos: ['us'] },
+ { name: 'deepseek.v3.2', stop_sequences: false },
+ { name: 'deepseek.r1-v1:0', inference_profile_geos: ['us'], stop_sequences: false },
+ { name: 'openai.gpt-oss-20b-1:0', stop_sequences: false },
+ { name: 'openai.gpt-oss-safeguard-20b', stop_sequences: false },
+ { name: 'writer.palmyra-x5-v1:0', inference_profile_geos: ['us'] },
+ { name: 'minimax.minimax-m2' },
+ { name: 'nvidia.nemotron-nano-9b-v2' },
+ { name: 'qwen.qwen3-32b-v1:0' },
+ { name: 'google.gemma-3-4b-it' },
+ { name: 'anthropic.claude-3-haiku-20240307-v1:0' }
+ ])
+}))
+
+beforeEach(() => {
+ _resetInferenceProfileCache()
+ _resetStopSeqCache()
+})
+
+// ---------------------------------------------------------------------------
+// validateEndpointHost
+// ---------------------------------------------------------------------------
+
+describe('validateEndpointHost', () => {
+ it('accepts a bare hostname', () => {
+ const result = validateEndpointHost('bedrock-runtime.us-east-1.amazonaws.com')
+ expect(result.hostname).toBe('bedrock-runtime.us-east-1.amazonaws.com')
+ expect(result.migratedArn).toBeUndefined()
+ })
+
+ it('accepts a hostname with port', () => {
+ const result = validateEndpointHost('bedrock-runtime.us-east-1.amazonaws.com:443')
+ expect(result.hostname).toBe('bedrock-runtime.us-east-1.amazonaws.com:443')
+ })
+
+ it('auto-migrates an ARN to migratedArn', () => {
+ const arn = 'arn:aws:bedrock:us-east-1:123456789012:provisioned-model/abc'
+ const result = validateEndpointHost(arn)
+ expect(result.hostname).toBeUndefined()
+ expect(result.migratedArn).toBe(arn)
+ })
+
+ it('extracts hostname from a URL with scheme', () => {
+ const result = validateEndpointHost('https://bedrock-runtime.us-east-1.amazonaws.com')
+ expect(result.hostname).toBe('bedrock-runtime.us-east-1.amazonaws.com')
+ expect(result.migratedArn).toBeUndefined()
+ })
+
+ it('extracts hostname before path separator', () => {
+ const result = validateEndpointHost('bedrock-runtime.us-east-1.amazonaws.com/model/invoke')
+ expect(result.hostname).toBe('bedrock-runtime.us-east-1.amazonaws.com')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// resolveBedrockModel
+// ---------------------------------------------------------------------------
+
+describe('regionToGeoCandidates', () => {
+ it('maps us- regions to [us]', () => expect(regionToGeoCandidates('us-east-1')).toEqual(['us']))
+ it('maps us-west-2 to [us]', () => expect(regionToGeoCandidates('us-west-2')).toEqual(['us']))
+ it('maps eu- regions to [eu]', () => expect(regionToGeoCandidates('eu-west-1')).toEqual(['eu']))
+ it('maps eu-central-1 to [eu]', () => expect(regionToGeoCandidates('eu-central-1')).toEqual(['eu']))
+ it('maps ap-southeast-1 to [apac]', () => expect(regionToGeoCandidates('ap-southeast-1')).toEqual(['apac']))
+ it('maps ap-northeast-1 (Tokyo) to [jp, apac]', () => expect(regionToGeoCandidates('ap-northeast-1')).toEqual(['jp', 'apac']))
+ it('maps ap-northeast-3 (Osaka) to [jp, apac]', () => expect(regionToGeoCandidates('ap-northeast-3')).toEqual(['jp', 'apac']))
+ it('maps ap-southeast-2 (Sydney) to [au, apac]', () => expect(regionToGeoCandidates('ap-southeast-2')).toEqual(['au', 'apac']))
+ it('maps ap-northeast-2 (Seoul) to [apac]', () => expect(regionToGeoCandidates('ap-northeast-2')).toEqual(['apac']))
+ it('maps ap-south-1 to [apac]', () => expect(regionToGeoCandidates('ap-south-1')).toEqual(['apac']))
+ it('maps ca-central-1 to [ca, us]', () => expect(regionToGeoCandidates('ca-central-1')).toEqual(['ca', 'us']))
+ it('maps sa-east-1 to [] (global fallback only)', () => expect(regionToGeoCandidates('sa-east-1')).toEqual([]))
+ it('defaults to [us] for unknown regions', () => expect(regionToGeoCandidates('me-south-1')).toEqual(['us']))
+})
+
+describe('resolveBedrockModel', () => {
+ const dropdown = 'anthropic.claude-3-haiku-20240307-v1:0'
+
+ // --- basic resolution ---
+ it('falls back to dropdown when customModel is empty (model not in map)', async () => {
+ const result = await resolveBedrockModel('', dropdown, 'us-east-1')
+ expect(result.modelId).toBe(dropdown)
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ it('falls back to dropdown when customModel is undefined', async () => {
+ const result = await resolveBedrockModel(undefined, dropdown, 'us-east-1')
+ expect(result.modelId).toBe(dropdown)
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ it('model not in map gets no profile', async () => {
+ const result = await resolveBedrockModel('google.gemma-3-4b-it', dropdown, 'us-east-1')
+ expect(result.modelId).toBe('google.gemma-3-4b-it')
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ // --- explicit user-supplied profiles (passthrough) ---
+ it('detects explicit us. profile in customModel', async () => {
+ const result = await resolveBedrockModel('us.anthropic.claude-sonnet-4-6', dropdown, 'us-east-1')
+ expect(result.modelId).toBe('anthropic.claude-sonnet-4-6')
+ expect(result.applicationInferenceProfile).toBe('us.anthropic.claude-sonnet-4-6')
+ })
+
+ it('detects explicit eu. profile in customModel', async () => {
+ const result = await resolveBedrockModel('eu.anthropic.claude-sonnet-4-6', dropdown, 'eu-west-1')
+ expect(result.applicationInferenceProfile).toBe('eu.anthropic.claude-sonnet-4-6')
+ })
+
+ it('detects explicit apac. profile in customModel', async () => {
+ const result = await resolveBedrockModel('apac.amazon.nova-pro-v1:0', dropdown, 'ap-southeast-1')
+ expect(result.modelId).toBe('amazon.nova-pro-v1:0')
+ expect(result.applicationInferenceProfile).toBe('apac.amazon.nova-pro-v1:0')
+ })
+
+ it('detects explicit jp. profile in customModel', async () => {
+ const result = await resolveBedrockModel('jp.anthropic.claude-sonnet-4-6', dropdown, 'ap-northeast-1')
+ expect(result.modelId).toBe('anthropic.claude-sonnet-4-6')
+ expect(result.applicationInferenceProfile).toBe('jp.anthropic.claude-sonnet-4-6')
+ })
+
+ it('detects explicit au. profile in customModel', async () => {
+ const result = await resolveBedrockModel('au.anthropic.claude-sonnet-4-6', dropdown, 'ap-southeast-2')
+ expect(result.modelId).toBe('anthropic.claude-sonnet-4-6')
+ expect(result.applicationInferenceProfile).toBe('au.anthropic.claude-sonnet-4-6')
+ })
+
+ it('detects explicit ca. profile in customModel', async () => {
+ const result = await resolveBedrockModel('ca.amazon.nova-lite-v1:0', dropdown, 'ca-central-1')
+ expect(result.modelId).toBe('amazon.nova-lite-v1:0')
+ expect(result.applicationInferenceProfile).toBe('ca.amazon.nova-lite-v1:0')
+ })
+
+ it('detects explicit global. profile in customModel', async () => {
+ const result = await resolveBedrockModel('global.anthropic.claude-opus-4-5-20251101-v1:0', dropdown, 'us-east-1')
+ expect(result.modelId).toBe('anthropic.claude-opus-4-5-20251101-v1:0')
+ expect(result.applicationInferenceProfile).toBe('global.anthropic.claude-opus-4-5-20251101-v1:0')
+ })
+
+ it('detects a full ARN', async () => {
+ const arn = 'arn:aws:bedrock:us-east-1:123456789012:inference-profile/abc'
+ const result = await resolveBedrockModel(arn, dropdown, 'us-east-1')
+ expect(result.modelId).toBe(dropdown)
+ expect(result.applicationInferenceProfile).toBe(arn)
+ })
+
+ // --- auto-apply: US region ---
+ it('auto-applies us. profile in us-east-1', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'us-east-1')
+ expect(result.applicationInferenceProfile).toBe('us.anthropic.claude-sonnet-4-6')
+ })
+
+ it('auto-applies us. for Nova in us-west-2', async () => {
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'us-west-2')
+ expect(result.applicationInferenceProfile).toBe('us.amazon.nova-pro-v1:0')
+ })
+
+ // --- auto-apply: EU region ---
+ it('auto-applies eu. profile when in eu-west-1 and model has eu', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'eu-west-1')
+ expect(result.applicationInferenceProfile).toBe('eu.anthropic.claude-sonnet-4-6')
+ })
+
+ it('auto-applies eu. for Nova Pro in eu-central-1', async () => {
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'eu-central-1')
+ expect(result.applicationInferenceProfile).toBe('eu.amazon.nova-pro-v1:0')
+ })
+
+ // --- auto-apply: APAC region (ap-southeast-1 → apac) ---
+ it('auto-applies apac. profile when in ap-southeast-1 and model has apac', async () => {
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'ap-southeast-1')
+ expect(result.applicationInferenceProfile).toBe('apac.amazon.nova-pro-v1:0')
+ })
+
+ // --- auto-apply: Tokyo (ap-northeast-1 → jp preferred, apac fallback) ---
+ it('auto-applies jp. in ap-northeast-1 when model has jp', async () => {
+ // claude-sonnet-4-6 has ['us','eu','jp','global'] → jp matches first
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'ap-northeast-1')
+ expect(result.applicationInferenceProfile).toBe('jp.anthropic.claude-sonnet-4-6')
+ })
+
+ it('falls back to apac. in ap-northeast-1 when model has apac but not jp', async () => {
+ // nova-pro has ['us','eu','apac'] → jp not found, apac matches
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'ap-northeast-1')
+ expect(result.applicationInferenceProfile).toBe('apac.amazon.nova-pro-v1:0')
+ })
+
+ // --- auto-apply: Osaka (ap-northeast-3 → jp, same as Tokyo) ---
+ it('auto-applies jp. in ap-northeast-3 (Osaka) same as Tokyo', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'ap-northeast-3')
+ expect(result.applicationInferenceProfile).toBe('jp.anthropic.claude-sonnet-4-6')
+ })
+
+ // --- auto-apply: Sydney (ap-southeast-2 → au preferred, apac fallback) ---
+ it('auto-applies au. in ap-southeast-2 when model has au', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'ap-southeast-2')
+ expect(result.applicationInferenceProfile).toBe('au.anthropic.claude-sonnet-4-6')
+ })
+
+ it('falls back to apac. in ap-southeast-2 when model has apac but not au', async () => {
+ // nova-pro has ['us','eu','apac'] → au not found, apac matches
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'ap-southeast-2')
+ expect(result.applicationInferenceProfile).toBe('apac.amazon.nova-pro-v1:0')
+ })
+
+ // --- fallback chain ---
+ it('falls back to global. when preferred geo not available', async () => {
+ // claude-opus-4-5 has ['us','eu','global'] but NOT apac or jp
+ const result = await resolveBedrockModel('', 'anthropic.claude-opus-4-5-20251101-v1:0', 'ap-southeast-1')
+ expect(result.applicationInferenceProfile).toBe('global.anthropic.claude-opus-4-5-20251101-v1:0')
+ })
+
+ it('falls back to global. in ap-northeast-1 when model has neither jp nor apac', async () => {
+ // claude-opus-4-5 has ['us','eu','global'] → jp miss, apac miss, global matches
+ const result = await resolveBedrockModel('', 'anthropic.claude-opus-4-5-20251101-v1:0', 'ap-northeast-1')
+ expect(result.applicationInferenceProfile).toBe('global.anthropic.claude-opus-4-5-20251101-v1:0')
+ })
+
+ // --- South America (sa-east-1 → only global profiles available) ---
+ it('falls back to global. in sa-east-1 (no sa. profiles exist)', async () => {
+ // claude-sonnet-4-6 has ['us','eu','jp','global'] → no SA candidates, global matches
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'sa-east-1')
+ expect(result.applicationInferenceProfile).toBe('global.anthropic.claude-sonnet-4-6')
+ })
+
+ it('falls back to us. in sa-east-1 when model has no global', async () => {
+ // meta.llama4-scout has ['us'] only → no SA candidates, no global, us matches
+ const result = await resolveBedrockModel('', 'meta.llama4-scout-17b-instruct-v1:0', 'sa-east-1')
+ expect(result.applicationInferenceProfile).toBe('us.meta.llama4-scout-17b-instruct-v1:0')
+ })
+
+ // --- Canada (ca-central-1 → ca preferred, then us fallback) ---
+ it('auto-applies ca. in ca-central-1 when model has ca', async () => {
+ // nova-lite has ['us','eu','apac','ca'] → ca matches first
+ const result = await resolveBedrockModel('', 'amazon.nova-lite-v1:0', 'ca-central-1')
+ expect(result.applicationInferenceProfile).toBe('ca.amazon.nova-lite-v1:0')
+ })
+
+ it('falls back to us. in ca-central-1 when model has no ca', async () => {
+ // claude-sonnet-4-6 has ['us','eu','jp','global'] → ca miss, us matches
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'ca-central-1')
+ expect(result.applicationInferenceProfile).toBe('us.anthropic.claude-sonnet-4-6')
+ })
+
+ it('falls back to us. when neither preferred geo nor global available', async () => {
+ // meta.llama4-scout has ['us'] only
+ const result = await resolveBedrockModel('', 'meta.llama4-scout-17b-instruct-v1:0', 'eu-west-1')
+ expect(result.applicationInferenceProfile).toBe('us.meta.llama4-scout-17b-instruct-v1:0')
+ })
+
+ it('falls back to us. for deepseek in apac region (us only model)', async () => {
+ const result = await resolveBedrockModel('', 'deepseek.r1-v1:0', 'ap-northeast-1')
+ expect(result.applicationInferenceProfile).toBe('us.deepseek.r1-v1:0')
+ })
+
+ // --- no profile models ---
+ it('does NOT auto-apply profile for MiniMax', async () => {
+ const result = await resolveBedrockModel('', 'minimax.minimax-m2', 'us-east-1')
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ it('does NOT auto-apply profile for NVIDIA', async () => {
+ const result = await resolveBedrockModel('', 'nvidia.nemotron-nano-9b-v2', 'us-east-1')
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ it('does NOT auto-apply profile for Qwen', async () => {
+ const result = await resolveBedrockModel('', 'qwen.qwen3-32b-v1:0', 'us-east-1')
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ // --- explicit profile overrides auto ---
+ it('does not override explicit profile with auto-mapping', async () => {
+ const result = await resolveBedrockModel('eu.anthropic.claude-sonnet-4-6', 'anthropic.claude-sonnet-4-6', 'us-east-1')
+ expect(result.applicationInferenceProfile).toBe('eu.anthropic.claude-sonnet-4-6')
+ })
+
+ // --- misc ---
+ it('trims whitespace from customModel', async () => {
+ const result = await resolveBedrockModel(' amazon.nova-pro-v1:0 ', dropdown, 'us-east-1')
+ expect(result.modelId).toBe('amazon.nova-pro-v1:0')
+ })
+
+ it('defaults to us-east-1 when region not provided', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6')
+ expect(result.applicationInferenceProfile).toBe('us.anthropic.claude-sonnet-4-6')
+ })
+
+ // --- model loader failure gracefully degrades ---
+ it('returns no profile when model loader fails', async () => {
+ const { getModels } = require('../../../src/modelLoader')
+ getModels.mockRejectedValueOnce(new Error('Network error'))
+ _resetInferenceProfileCache()
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'us-east-1')
+ expect(result.modelId).toBe('anthropic.claude-sonnet-4-6')
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ // --- runtime discovery: availableProfiles parameter ---
+ it('applies profile when confirmed available in region', async () => {
+ const available = new Set(['eu.anthropic.claude-sonnet-4-6', 'global.anthropic.claude-sonnet-4-6'])
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'eu-west-1', available)
+ expect(result.applicationInferenceProfile).toBe('eu.anthropic.claude-sonnet-4-6')
+ })
+
+ it('skips profile when NOT available in region, falls back to global', async () => {
+ // eu-west-2 has global but NOT eu.amazon.nova-pro
+ const available = new Set(['global.amazon.nova-2-lite-v1:0', 'eu.anthropic.claude-sonnet-4-6'])
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'eu-west-2', available)
+ // nova-pro has geos ['us','eu','apac'] — eu. not in available, no global for this model, try us.
+ // us.amazon.nova-pro not in available either → no profile (direct invocation)
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ it('falls back to global. when eu. not available but global. is', async () => {
+ const available = new Set(['global.amazon.nova-2-lite-v1:0'])
+ const result = await resolveBedrockModel('', 'amazon.nova-2-lite-v1:0', 'eu-west-2', available)
+ expect(result.applicationInferenceProfile).toBe('global.amazon.nova-2-lite-v1:0')
+ })
+
+ it('still applies explicit profile from customModel regardless of availableProfiles', async () => {
+ const available = new Set() // empty — nothing available
+ const result = await resolveBedrockModel('eu.anthropic.claude-sonnet-4-6', 'anthropic.claude-sonnet-4-6', 'eu-west-2', available)
+ // Explicit profile bypasses runtime check
+ expect(result.applicationInferenceProfile).toBe('eu.anthropic.claude-sonnet-4-6')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// useGlobalEndpoint override
+// ---------------------------------------------------------------------------
+describe('useGlobalEndpoint override', () => {
+ beforeEach(() => {
+ _resetInferenceProfileCache()
+ })
+
+ it('forces global.* when useGlobalEndpoint=true and model has global geo', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'eu-west-1', undefined, true)
+ expect(result.applicationInferenceProfile).toBe('global.anthropic.claude-sonnet-4-6')
+ })
+
+ it('falls back to regional when useGlobalEndpoint=true but model has no global geo', async () => {
+ const result = await resolveBedrockModel('', 'amazon.nova-pro-v1:0', 'eu-west-1', undefined, true)
+ // nova-pro geos: us, eu, apac — no global. Should fall back to eu.
+ expect(result.applicationInferenceProfile).toBe('eu.amazon.nova-pro-v1:0')
+ })
+
+ it('uses normal regional selection when useGlobalEndpoint=false', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'eu-west-1', undefined, false)
+ expect(result.applicationInferenceProfile).toBe('eu.anthropic.claude-sonnet-4-6')
+ })
+
+ it('uses normal regional selection when useGlobalEndpoint=undefined', async () => {
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'ap-northeast-1', undefined, undefined)
+ expect(result.applicationInferenceProfile).toBe('jp.anthropic.claude-sonnet-4-6')
+ })
+
+ it('global override with runtime discovery validates profile exists', async () => {
+ const available = new Set(['global.anthropic.claude-sonnet-4-6'])
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'eu-west-1', available, true)
+ expect(result.applicationInferenceProfile).toBe('global.anthropic.claude-sonnet-4-6')
+ })
+
+ it('global override with runtime discovery falls back when global not in region', async () => {
+ const available = new Set(['eu.anthropic.claude-sonnet-4-6']) // global not available
+ const result = await resolveBedrockModel('', 'anthropic.claude-sonnet-4-6', 'eu-west-1', available, true)
+ // global not in available set, allCandidateGeos is just ['global'], nothing matches
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+
+ it('does not affect models with no inference profiles', async () => {
+ const result = await resolveBedrockModel('', 'minimax.minimax-m2', 'us-east-1', undefined, true)
+ expect(result.applicationInferenceProfile).toBeUndefined()
+ })
+})
+
+// ---------------------------------------------------------------------------
+// getStopSeqUnsupportedModels
+// ---------------------------------------------------------------------------
+
+describe('getStopSeqUnsupportedModels', () => {
+ it('returns models with stop_sequences: false', async () => {
+ const set = await getStopSeqUnsupportedModels()
+ expect(set.has('deepseek.v3.2')).toBe(true)
+ expect(set.has('deepseek.r1-v1:0')).toBe(true)
+ expect(set.has('openai.gpt-oss-20b-1:0')).toBe(true)
+ expect(set.has('openai.gpt-oss-safeguard-20b')).toBe(true)
+ })
+
+ it('does NOT include models without the flag', async () => {
+ const set = await getStopSeqUnsupportedModels()
+ expect(set.has('anthropic.claude-sonnet-4-6')).toBe(false)
+ expect(set.has('amazon.nova-pro-v1:0')).toBe(false)
+ expect(set.has('minimax.minimax-m2')).toBe(false)
+ })
+})
+
+// ---------------------------------------------------------------------------
+// normalizeBedrockError
+// ---------------------------------------------------------------------------
+
+describe('normalizeBedrockError', () => {
+ it('rewrites inference-profile-required errors', () => {
+ const err = new Error(
+ "ValidationException: Invocation of model ID anthropic.claude-sonnet-4-6 with on-demand throughput isn't supported."
+ )
+ const result = normalizeBedrockError(err)
+ expect(result.message).toContain('inference profile')
+ expect(result.message).toContain('Model Name')
+ })
+
+ it('rewrites Converse-unsupported errors', () => {
+ const err = new Error("This model doesn't support the Converse operation")
+ const result = normalizeBedrockError(err)
+ expect(result.message).toContain('InvokeModel')
+ expect(result.message).toContain('Custom Model ARN')
+ })
+
+ it('rewrites unsupported-field errors', () => {
+ const err = new Error("This model doesn't support the stopSequences field")
+ const result = normalizeBedrockError(err)
+ expect(result.message).toContain('request parameter')
+ })
+
+ it('returns unrecognized errors unchanged', () => {
+ const err = new Error('Some other random error')
+ const result = normalizeBedrockError(err)
+ expect(result).toBe(err)
+ })
+
+ it('handles non-Error values', () => {
+ const result = normalizeBedrockError('string error' as any)
+ expect(result).toBe('string error')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// BedrockChat error normalization wiring
+// ---------------------------------------------------------------------------
+
+describe('BedrockChat error normalization wiring', () => {
+ let BedrockChat: any
+
+ beforeAll(async () => {
+ // Dynamic import to pick up the jest mock for modelLoader
+ const mod = await import('./FlowiseAWSChatBedrock')
+ BedrockChat = mod.BedrockChat
+ })
+
+ it('_generate normalizes inference-profile-required errors', async () => {
+ const chat = new BedrockChat('test-id', { model: 'anthropic.claude-sonnet-4-6', region: 'us-east-1' })
+
+ // Mock the parent _generate to throw a raw Bedrock error
+ const originalGenerate = Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = jest
+ .fn()
+ .mockRejectedValue(
+ new Error(
+ "ValidationException: Invocation of model ID anthropic.claude-sonnet-4-6 with on-demand throughput isn't supported."
+ )
+ )
+
+ try {
+ await chat._generate([], {})
+ fail('Expected error to be thrown')
+ } catch (err: any) {
+ expect(err.message).toContain('cross-region inference profile')
+ expect(err.message).toContain('Model Name')
+ }
+
+ // Restore
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = originalGenerate
+ })
+
+ it('_generate normalizes Converse-unsupported errors', async () => {
+ const chat = new BedrockChat('test-id', { model: 'custom-model', region: 'us-east-1' })
+
+ const originalGenerate = Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = jest
+ .fn()
+ .mockRejectedValue(new Error("This model doesn't support the Converse operation"))
+
+ try {
+ await chat._generate([], {})
+ fail('Expected error to be thrown')
+ } catch (err: any) {
+ expect(err.message).toContain('InvokeModel')
+ expect(err.message).toContain('Custom Model ARN')
+ }
+
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = originalGenerate
+ })
+
+ it('_generate passes through unrecognized errors unchanged', async () => {
+ const chat = new BedrockChat('test-id', { model: 'some-model', region: 'us-east-1' })
+
+ const rawError = new Error('Something completely unexpected')
+ const originalGenerate = Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = jest.fn().mockRejectedValue(rawError)
+
+ try {
+ await chat._generate([], {})
+ fail('Expected error to be thrown')
+ } catch (err: any) {
+ expect(err).toBe(rawError)
+ }
+
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = originalGenerate
+ })
+
+ it('_generate does not interfere with successful responses', async () => {
+ const chat = new BedrockChat('test-id', { model: 'some-model', region: 'us-east-1' })
+
+ const mockResult = { generations: [{ text: 'hello', message: { content: 'hello' } }] }
+ const originalGenerate = Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = jest.fn().mockResolvedValue(mockResult)
+
+ const result = await chat._generate([], {})
+ expect(result).toBe(mockResult)
+
+ Object.getPrototypeOf(Object.getPrototypeOf(chat))._generate = originalGenerate
+ })
+
+ it('stopSeqUnsupported strips stopSequences for flagged models', () => {
+ const unsupported = new Set(['deepseek.r1-v1:0'])
+ const chat = new BedrockChat('test-id', { model: 'deepseek.r1-v1:0', region: 'us-east-1' }, unsupported)
+
+ // Mock super.invocationParams to return params with stopSequences
+ const originalInvocationParams = Object.getPrototypeOf(Object.getPrototypeOf(chat)).invocationParams
+ Object.getPrototypeOf(Object.getPrototypeOf(chat)).invocationParams = jest.fn().mockReturnValue({
+ inferenceConfig: { maxTokens: 200, stopSequences: ['\\n'] }
+ })
+
+ const params = chat.invocationParams({})
+ expect(params.inferenceConfig.stopSequences).toBeUndefined()
+ expect(params.inferenceConfig.maxTokens).toBe(200)
+
+ Object.getPrototypeOf(Object.getPrototypeOf(chat)).invocationParams = originalInvocationParams
+ })
+
+ it('stopSeqUnsupported keeps stopSequences for non-flagged models', () => {
+ const unsupported = new Set(['deepseek.r1-v1:0'])
+ const chat = new BedrockChat('test-id', { model: 'anthropic.claude-sonnet-4-6', region: 'us-east-1' }, unsupported)
+
+ const originalInvocationParams = Object.getPrototypeOf(Object.getPrototypeOf(chat)).invocationParams
+ Object.getPrototypeOf(Object.getPrototypeOf(chat)).invocationParams = jest.fn().mockReturnValue({
+ inferenceConfig: { maxTokens: 200, stopSequences: ['\\n'] }
+ })
+
+ const params = chat.invocationParams({})
+ expect(params.inferenceConfig.stopSequences).toEqual(['\\n'])
+
+ Object.getPrototypeOf(Object.getPrototypeOf(chat)).invocationParams = originalInvocationParams
+ })
+})
+
+// ---------------------------------------------------------------------------
+// Imported model auto-detection — ARN pattern matching
+// ---------------------------------------------------------------------------
+describe('Imported model ARN detection', () => {
+ it('matches imported-model ARNs', () => {
+ expect('arn:aws:bedrock:us-east-1:123456789000:imported-model/s8s5k3tqgk1b'.includes(':imported-model/')).toBe(true)
+ })
+
+ it('does not match provisioned-model ARNs', () => {
+ expect('arn:aws:bedrock:us-east-1:123456789000:provisioned-model/my-pt'.includes(':imported-model/')).toBe(false)
+ })
+
+ it('does not match inference-profile ARNs', () => {
+ expect('arn:aws:bedrock:us-east-1:123456789000:inference-profile/abc'.includes(':imported-model/')).toBe(false)
+ })
+
+ it('does not match plain model IDs', () => {
+ expect('anthropic.claude-sonnet-4-6'.includes(':imported-model/')).toBe(false)
+ })
+
+ it('does not match geo-prefixed inference profile IDs', () => {
+ expect('us.anthropic.claude-sonnet-4-6'.includes(':imported-model/')).toBe(false)
+ })
+
+ it('does not match custom-model (fine-tuned) ARNs', () => {
+ expect('arn:aws:bedrock:us-east-1:123456789000:custom-model/my-ft'.includes(':imported-model/')).toBe(false)
+ })
+})
+
+// ---------------------------------------------------------------------------
+// Imported model auto-detection — full init() routing
+//
+// These tests verify that the Converse node's init() correctly routes to
+// BedrockImportedChat or BedrockChat depending on the model type.
+// ---------------------------------------------------------------------------
+describe('Converse node init() routing for custom models', () => {
+ // We can't easily call init() directly (it needs INodeData, options, etc.),
+ // so we test the routing logic extracted into a helper that mirrors init().
+ // This function replicates the decision tree in AWSChatBedrock.ts init().
+ function simulateInitRouting(params: {
+ customModel?: string
+ endpointHost?: string
+ instructSupported?: boolean
+ supportedFormats?: string[]
+ }): 'BedrockImportedChat' | 'BedrockChat' | 'ValidationError' {
+ let endpointMigratedArn: string | undefined
+ if (params.endpointHost) {
+ const result = validateEndpointHost(params.endpointHost)
+ endpointMigratedArn = result.migratedArn
+ }
+
+ const effectiveModel = endpointMigratedArn || params.customModel
+ if (effectiveModel && !effectiveModel.startsWith('arn:aws:bedrock:')) {
+ return 'ValidationError'
+ }
+ if (effectiveModel?.includes(':imported-model/')) {
+ if (params.instructSupported !== true) {
+ return 'BedrockImportedChat'
+ }
+ }
+ return 'BedrockChat'
+ }
+
+ // --- Imported model ARN in customModel ---
+
+ it('imported ARN + instructSupported:false → BedrockImportedChat', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'arn:aws:bedrock:us-east-1:123:imported-model/xyz',
+ instructSupported: false
+ })
+ ).toBe('BedrockImportedChat')
+ })
+
+ it('imported ARN + instructSupported:undefined → BedrockImportedChat', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'arn:aws:bedrock:us-east-1:123:imported-model/xyz',
+ instructSupported: undefined
+ })
+ ).toBe('BedrockImportedChat')
+ })
+
+ it('imported ARN + instructSupported:true → BedrockChat (Converse)', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'arn:aws:bedrock:us-east-1:123:imported-model/xyz',
+ instructSupported: true
+ })
+ ).toBe('BedrockChat')
+ })
+
+ // --- Imported model ARN via endpointHost auto-migration ---
+
+ it('imported ARN in endpointHost + instructSupported:false → BedrockImportedChat', () => {
+ expect(
+ simulateInitRouting({
+ endpointHost: 'arn:aws:bedrock:us-east-1:123:imported-model/xyz',
+ instructSupported: false
+ })
+ ).toBe('BedrockImportedChat')
+ })
+
+ it('imported ARN in endpointHost overrides customModel', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'anthropic.claude-sonnet-4-6',
+ endpointHost: 'arn:aws:bedrock:us-east-1:123:imported-model/xyz',
+ instructSupported: false
+ })
+ ).toBe('BedrockImportedChat')
+ })
+
+ // --- Non-imported model types → always Converse ---
+
+ it('provisioned-model ARN → BedrockChat', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'arn:aws:bedrock:us-east-1:123:provisioned-model/my-pt'
+ })
+ ).toBe('BedrockChat')
+ })
+
+ it('inference-profile ARN → BedrockChat', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'arn:aws:bedrock:us-east-1:123:inference-profile/abc'
+ })
+ ).toBe('BedrockChat')
+ })
+
+ it('custom-model (fine-tuned) ARN → BedrockChat', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'arn:aws:bedrock:us-east-1:123:custom-model/my-ft'
+ })
+ ).toBe('BedrockChat')
+ })
+
+ it('geo-prefixed inference profile in customModel → ValidationError (use dropdown)', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'us.anthropic.claude-sonnet-4-6'
+ })
+ ).toBe('ValidationError')
+ })
+
+ it('plain model ID in customModel → ValidationError (use dropdown)', () => {
+ expect(
+ simulateInitRouting({
+ customModel: 'anthropic.claude-sonnet-4-6'
+ })
+ ).toBe('ValidationError')
+ })
+
+ it('empty customModel → BedrockChat (dropdown fallback)', () => {
+ expect(
+ simulateInitRouting({
+ customModel: ''
+ })
+ ).toBe('BedrockChat')
+ })
+
+ it('undefined customModel → BedrockChat', () => {
+ expect(
+ simulateInitRouting({
+ customModel: undefined
+ })
+ ).toBe('BedrockChat')
+ })
+
+ it('bare hostname in endpointHost (not ARN) → BedrockChat', () => {
+ expect(
+ simulateInitRouting({
+ endpointHost: 'bedrock-runtime.us-east-1.amazonaws.com'
+ })
+ ).toBe('BedrockChat')
+ })
+
+ // --- Input validation: reject model names that aren't ARNs ---
+
+ it('model name (not ARN) → ValidationError', () => {
+ expect(simulateInitRouting({ customModel: 'SantaCoder-1B' })).toBe('ValidationError')
+ })
+
+ it('model name with trailing dot → ValidationError', () => {
+ expect(simulateInitRouting({ customModel: 'jcvze99n15u4.' })).toBe('ValidationError')
+ })
+
+ it('bare hash ID → ValidationError', () => {
+ expect(simulateInitRouting({ customModel: 'jcvze99n15u4' })).toBe('ValidationError')
+ })
+
+ it('built-in model ID (use dropdown instead) → ValidationError', () => {
+ expect(simulateInitRouting({ customModel: 'anthropic.claude-sonnet-4-6' })).toBe('ValidationError')
+ })
+
+ it('geo-prefixed ID (auto-applied, not manual) → ValidationError', () => {
+ expect(simulateInitRouting({ customModel: 'us.anthropic.claude-sonnet-4-6' })).toBe('ValidationError')
+ })
+
+ it('random string → ValidationError', () => {
+ expect(simulateInitRouting({ customModel: 'My Custom Model' })).toBe('ValidationError')
+ })
+
+ it('valid imported-model ARN → accepted', () => {
+ expect(simulateInitRouting({ customModel: 'arn:aws:bedrock:us-east-1:123:imported-model/xyz', instructSupported: false })).toBe(
+ 'BedrockImportedChat'
+ )
+ })
+
+ it('valid provisioned-model ARN → accepted', () => {
+ expect(simulateInitRouting({ customModel: 'arn:aws:bedrock:us-east-1:123:provisioned-model/xyz' })).toBe('BedrockChat')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// Format detection for imported models
+// ---------------------------------------------------------------------------
+describe('detectFormat for imported models', () => {
+ it('prefers openai-chat-completion when ChatCompletionRequest available', () => {
+ expect(detectFormat(['chatcompletionrequest', 'bedrockmetacompletionrequest'])).toBe('openai-chat-completion')
+ })
+
+ it('returns bedrock-completion when only prompt-based formats exist', () => {
+ expect(detectFormat(['completionrequest'])).toBe('bedrock-completion')
+ })
+
+ it('returns bedrock-completion when only BedrockCompletionRequest exists', () => {
+ expect(detectFormat(['bedrockcompletionrequest'])).toBe('bedrock-completion')
+ })
+
+ it('defaults to openai-chat-completion when no formats provided', () => {
+ expect(detectFormat(undefined)).toBe('openai-chat-completion')
+ })
+
+ it('defaults to openai-chat-completion for empty array', () => {
+ expect(detectFormat([])).toBe('openai-chat-completion')
+ })
+})
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts
index 4a13be48e..c48f09ec6 100644
--- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts
+++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts
@@ -5,6 +5,8 @@ import { getModels, getRegions, MODEL_TYPE } from '../../../src/modelLoader'
import { getAWSCredentialConfig } from '../../../src/awsToolsUtils'
import { ChatBedrockConverseInput, ChatBedrockConverse } from '@langchain/aws'
import { BedrockChat } from './FlowiseAWSChatBedrock'
+import { validateEndpointHost, resolveBedrockModel, discoverInferenceProfiles, getStopSeqUnsupportedModels } from './utils'
+import { BedrockImportedChat, getImportedModelInfo, detectFormat } from './FlowiseAWSChatBedrockImported'
import { supportsSamplingParams } from '../../../src/anthropicUtils'
class AWSChatBedrock_ChatModels implements INode {
@@ -26,7 +28,8 @@ class AWSChatBedrock_ChatModels implements INode {
this.type = 'AWSChatBedrock'
this.icon = 'aws.svg'
this.category = 'Chat Models'
- this.description = 'Wrapper around AWS Bedrock large language models that use the Converse API'
+ this.description =
+ 'Wrapper around AWS Bedrock large language models. Supports built-in, imported, fine-tuned, and provisioned-throughput models.'
this.baseClasses = [this.type, ...getBaseClasses(ChatBedrockConverse)]
this.credential = {
label: 'AWS Credential',
@@ -54,12 +57,19 @@ class AWSChatBedrock_ChatModels implements INode {
name: 'model',
type: 'asyncOptions',
loadMethod: 'listModels',
- default: 'anthropic.claude-3-haiku-20240307-v1:0'
+ default: 'anthropic.claude-haiku-4-5-20251001-v1:0',
+ optional: true
},
{
- label: 'Custom Model Name',
+ label: 'Custom Model ARN',
name: 'customModel',
- description: 'If provided, will override model selected from Model Name option',
+ description:
+ 'For imported, fine-tuned, or provisioned-throughput models. ' +
+ 'Enter the full ARN. Imported models are auto-detected and routed to the correct API. ' +
+ 'For fine-tuned models, use the deployment ARN (custom-model-deployment/... for on-demand, ' +
+ 'or provisioned-model/... for Provisioned Throughput) — the raw custom-model/... artifact ARN is not invokable. ' +
+ 'For built-in models, use the dropdown above instead.',
+ placeholder: 'arn:aws:bedrock:us-east-1:123456:imported-model/my-model',
type: 'string',
optional: true
},
@@ -67,8 +77,12 @@ class AWSChatBedrock_ChatModels implements INode {
label: 'Custom Endpoint Host',
name: 'endpointHost',
type: 'string',
- description: 'Custom endpoint host to use for the model. If provided, will override the default endpoint host.',
- optional: true
+ description:
+ 'Hostname-only override for a custom VPC endpoint or proxy ' +
+ '(e.g. bedrock-runtime.us-east-1.amazonaws.com). ' +
+ 'Do NOT enter model ARNs or inference profile IDs here.',
+ optional: true,
+ additionalParams: true
},
{
label: 'Streaming',
@@ -116,6 +130,17 @@ class AWSChatBedrock_ChatModels implements INode {
default: false,
optional: true,
additionalParams: true
+ },
+ {
+ label: 'Use Global Inference Endpoint',
+ name: 'useGlobalEndpoint',
+ type: 'boolean',
+ description:
+ 'Force the global cross-region inference profile instead of the region-specific one. ' +
+ 'Bedrock routes dynamically across regions for maximum availability.',
+ default: false,
+ optional: true,
+ additionalParams: true
}
]
}
@@ -130,6 +155,21 @@ class AWSChatBedrock_ChatModels implements INode {
}
}
+ /**
+ * Initializes a BedrockChat instance for the Converse API.
+ *
+ * Flow:
+ * 1. Sanitize endpointHost (auto-migrate ARNs placed there by mistake)
+ * 2. Resolve AWS credentials (UI creds, AssumeRole, or SDK default chain)
+ * 3. Discover inference profiles available in the target region (cached)
+ * 4. Resolve model ID + optional inference profile from user input
+ * 5. Build ChatBedrockConverseInput and return BedrockChat instance
+ *
+ * Credentials are resolved BEFORE model resolution so that
+ * discoverInferenceProfiles() can reuse the same creds.
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
+ */
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise {
const iRegion = nodeData.inputs?.region as string
const iModel = nodeData.inputs?.model as string
@@ -139,30 +179,15 @@ class AWSChatBedrock_ChatModels implements INode {
const cache = nodeData.inputs?.cache as BaseCache
const streaming = nodeData.inputs?.streaming as boolean
const latencyOptimized = nodeData.inputs?.latencyOptimized as boolean
+ const useGlobalEndpoint = nodeData.inputs?.useGlobalEndpoint as boolean
const endpointHost = (nodeData.inputs?.endpointHost as string)?.trim()
- const resolvedModel = customModel ? customModel : iModel
-
- const obj: ChatBedrockConverseInput = {
- region: iRegion,
- model: resolvedModel,
- maxTokens: parseInt(iMax_tokens_to_sample, 10),
- streaming: streaming ?? true
- }
-
- // Newer Anthropic Claude models (Opus 4.7+) don't accept sampling
- // parameters. AWS Bedrock surfaces those models with names like
- // "anthropic.claude-opus-4-7-v1" or "us.anthropic.claude-opus-4-7-...".
- if (supportsSamplingParams(resolvedModel)) {
- obj.temperature = parseFloat(iTemperature)
- }
-
- if (latencyOptimized) {
- obj.performanceConfig = { latency: 'optimized' }
- }
-
+ let sanitizedEndpointHost: string | undefined
+ let endpointMigratedArn: string | undefined
if (endpointHost) {
- obj.endpointHost = endpointHost
+ const result = validateEndpointHost(endpointHost)
+ sanitizedEndpointHost = result.hostname
+ endpointMigratedArn = result.migratedArn
}
/**
@@ -174,6 +199,84 @@ class AWSChatBedrock_ChatModels implements INode {
* @see https://github.com/aws/aws-sdk-js-v3/blob/main/packages/credential-provider-node/README.md
*/
const credentialConfig = await getAWSCredentialConfig(nodeData, options, iRegion)
+
+ const effectiveModel = endpointMigratedArn || customModel
+ if (effectiveModel && !effectiveModel.startsWith('arn:aws:bedrock:')) {
+ throw new Error(
+ `"${effectiveModel}" is not a valid ARN. ` +
+ `The "Custom Model ARN" field requires a full Bedrock ARN ` +
+ `(e.g. arn:aws:bedrock:us-east-1:123456789000:imported-model/my-model). ` +
+ `For built-in models, use the "Model Name" dropdown instead.`
+ )
+ }
+
+ // Auto-detect imported models and route to InvokeModel API.
+ if (effectiveModel?.includes(':imported-model/')) {
+ const modelInfo = await getImportedModelInfo(effectiveModel, iRegion, credentialConfig.credentials)
+ if (modelInfo.instructSupported !== true) {
+ const format = detectFormat(modelInfo.supportedFormats)
+ const chat = new BedrockImportedChat(nodeData.id, {
+ region: iRegion,
+ modelId: effectiveModel,
+ format,
+ temperature: !isNaN(parseFloat(iTemperature)) ? parseFloat(iTemperature) : 0.7,
+ maxTokens: !isNaN(parseInt(iMax_tokens_to_sample, 10)) ? parseInt(iMax_tokens_to_sample, 10) : 200,
+ streaming: streaming ?? true,
+ credentials: credentialConfig.credentials
+ })
+ const multiModalOption: IMultiModalOption = {
+ image: { allowImageUploads: (nodeData.inputs?.allowImageUploads as boolean) ?? false }
+ }
+ chat.setMultiModalOption(multiModalOption)
+ return chat
+ }
+ // instructSupported: true — fall through to Converse path
+ }
+
+ // Discover which inference profiles actually exist in this region.
+ // Uses the same credentials the user configured (UI, AssumeRole, or SDK default).
+ const availableProfiles = await discoverInferenceProfiles(iRegion, credentialConfig.credentials)
+
+ // Resolve model ID and optional inference profile from customModel input.
+ // If an ARN was auto-migrated from endpointHost, it takes precedence
+ // over customModel because the original flow was misconfigured --
+ // the ARN was the real model target, customModel was likely a
+ // friendly label (e.g. "nova-custom") that isn't a valid Bedrock ID.
+ const effectiveCustomModel = endpointMigratedArn || customModel
+ const { modelId, applicationInferenceProfile } = await resolveBedrockModel(
+ effectiveCustomModel,
+ iModel,
+ iRegion,
+ availableProfiles,
+ useGlobalEndpoint
+ )
+
+ const obj: ChatBedrockConverseInput = {
+ region: iRegion,
+ model: modelId,
+ maxTokens: !isNaN(parseInt(iMax_tokens_to_sample, 10)) ? parseInt(iMax_tokens_to_sample, 10) : 200,
+ streaming: streaming ?? true
+ }
+
+ // Newer Anthropic Claude models (Opus 4.7+) don't accept sampling
+ // parameters. AWS Bedrock surfaces those models with names like
+ // "anthropic.claude-opus-4-7-v1" or "us.anthropic.claude-opus-4-7-...".
+ if (supportsSamplingParams(modelId)) {
+ obj.temperature = !isNaN(parseFloat(iTemperature)) ? parseFloat(iTemperature) : 0.7
+ }
+
+ if (applicationInferenceProfile) {
+ obj.applicationInferenceProfile = applicationInferenceProfile
+ }
+
+ if (latencyOptimized) {
+ obj.performanceConfig = { latency: 'optimized' }
+ }
+
+ if (sanitizedEndpointHost) {
+ obj.endpointHost = sanitizedEndpointHost
+ }
+
if (credentialConfig.credentials) {
obj.credentials = credentialConfig.credentials
}
@@ -187,7 +290,8 @@ class AWSChatBedrock_ChatModels implements INode {
}
}
- const amazonBedrock = new BedrockChat(nodeData.id, obj)
+ const stopSeqUnsupported = await getStopSeqUnsupportedModels()
+ const amazonBedrock = new BedrockChat(nodeData.id, obj, stopSeqUnsupported)
amazonBedrock.setMultiModalOption(multiModalOption)
return amazonBedrock
}
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrockImported.test.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrockImported.test.ts
new file mode 100644
index 000000000..3b414d485
--- /dev/null
+++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrockImported.test.ts
@@ -0,0 +1,455 @@
+import { BedrockImportedChat, detectFormat, getImportedModelInfo, _resetModelInfoCache } from './FlowiseAWSChatBedrockImported'
+import { HumanMessage, SystemMessage, AIMessage, ToolMessage } from '@langchain/core/messages'
+
+// ---------------------------------------------------------------------------
+// Mock AWS SDK clients
+// ---------------------------------------------------------------------------
+const mockSendRuntime = jest.fn()
+jest.mock('@aws-sdk/client-bedrock-runtime', () => ({
+ BedrockRuntimeClient: jest.fn().mockImplementation(() => ({
+ send: mockSendRuntime
+ })),
+ InvokeModelCommand: jest.fn().mockImplementation((input) => ({ ...input, _type: 'InvokeModelCommand' })),
+ InvokeModelWithResponseStreamCommand: jest
+ .fn()
+ .mockImplementation((input) => ({ ...input, _type: 'InvokeModelWithResponseStreamCommand' }))
+}))
+
+const mockSendBedrock = jest.fn()
+jest.mock('@aws-sdk/client-bedrock', () => ({
+ BedrockClient: jest.fn().mockImplementation(() => ({
+ send: mockSendBedrock
+ })),
+ GetImportedModelCommand: jest.fn().mockImplementation((input) => ({ ...input, _type: 'GetImportedModelCommand' }))
+}))
+
+function makeInvokeResponse(body: object) {
+ return { body: new TextEncoder().encode(JSON.stringify(body)) }
+}
+
+function makeStreamResponse(chunks: object[]) {
+ return {
+ body: (async function* () {
+ for (const chunk of chunks) {
+ yield { chunk: { bytes: new TextEncoder().encode(JSON.stringify(chunk)) } }
+ }
+ })()
+ }
+}
+
+// ---------------------------------------------------------------------------
+// detectFormat
+// ---------------------------------------------------------------------------
+describe('detectFormat', () => {
+ it('prefers openai-chat-completion when ChatCompletionRequest is supported', () => {
+ expect(detectFormat(['chatcompletionrequest', 'bedrockmetacompletionrequest'])).toBe('openai-chat-completion')
+ })
+
+ it('returns openai-chat-completion for ChatCompletion variant names', () => {
+ expect(detectFormat(['responsesrequest', 'chatcompletionrequest', 'completionrequest'])).toBe('openai-chat-completion')
+ })
+
+ it('returns bedrock-completion when only BedrockCompletion is available', () => {
+ expect(detectFormat(['bedrockcompletionrequest'])).toBe('bedrock-completion')
+ })
+
+ it('returns bedrock-completion when no chat completion format exists', () => {
+ expect(detectFormat(['completionrequest', 'embeddingchatrequest'])).toBe('bedrock-completion')
+ })
+
+ it('defaults to openai-chat-completion when no formats provided', () => {
+ expect(detectFormat(undefined)).toBe('openai-chat-completion')
+ })
+
+ it('defaults to openai-chat-completion for empty array', () => {
+ expect(detectFormat([])).toBe('openai-chat-completion')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// getImportedModelInfo
+// ---------------------------------------------------------------------------
+describe('getImportedModelInfo', () => {
+ beforeEach(() => {
+ _resetModelInfoCache()
+ mockSendBedrock.mockReset()
+ mockSendRuntime.mockReset()
+ })
+
+ it('returns model metadata and probed formats', async () => {
+ mockSendBedrock.mockResolvedValueOnce({
+ instructSupported: false,
+ modelArchitecture: 'llama'
+ })
+ mockSendRuntime.mockRejectedValueOnce(
+ new Error(
+ 'ValidationException: Available for this model: ChatCompletionRequest, BedrockMetaCompletionRequest, CompletionRequest'
+ )
+ )
+ const info = await getImportedModelInfo('arn:aws:bedrock:us-east-1:123:imported-model/test', 'us-east-1')
+ expect(info.instructSupported).toBe(false)
+ expect(info.modelArchitecture).toBe('llama')
+ expect(info.supportedFormats).toContain('chatcompletionrequest')
+ expect(info.supportedFormats).toContain('bedrockmetacompletionrequest')
+ })
+
+ it('caches results per model ID', async () => {
+ mockSendBedrock.mockResolvedValueOnce({ instructSupported: false, modelArchitecture: 'llama' })
+ mockSendRuntime.mockRejectedValueOnce(new Error('ValidationException: Available for this model: ChatCompletionRequest'))
+ const arn = 'arn:aws:bedrock:us-east-1:123:imported-model/cached'
+ await getImportedModelInfo(arn, 'us-east-1')
+ await getImportedModelInfo(arn, 'us-east-1')
+ expect(mockSendBedrock).toHaveBeenCalledTimes(1)
+ expect(mockSendRuntime).toHaveBeenCalledTimes(1)
+ })
+
+ it('throws when GetImportedModel fails', async () => {
+ mockSendBedrock.mockRejectedValueOnce(new Error('ValidationException: model not found'))
+ await expect(getImportedModelInfo('bad-arn', 'us-east-1')).rejects.toThrow('ValidationException')
+ })
+
+ it('throws when probe fails with non-format error', async () => {
+ mockSendBedrock.mockResolvedValueOnce({ instructSupported: false, modelArchitecture: 'llama' })
+ mockSendRuntime.mockRejectedValueOnce(new Error('InternalServerException: unexpected error'))
+ await expect(getImportedModelInfo('some-arn', 'us-east-1')).rejects.toThrow('InternalServerException')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// BedrockImportedChat — message conversion
+// ---------------------------------------------------------------------------
+describe('BedrockImportedChat message conversion', () => {
+ function makeChat(format = 'bedrock-completion' as any) {
+ return new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format,
+ temperature: 0.7,
+ maxTokens: 200,
+ streaming: false
+ })
+ }
+
+ describe('convertMessagesToPrompt (BedrockCompletion)', () => {
+ const chat = makeChat('bedrock-completion')
+
+ it('converts basic user message', () => {
+ const result = chat.convertMessagesToPrompt([new HumanMessage('Hello')])
+ expect(result).toBe('User: Hello\nAssistant:')
+ })
+
+ it('converts system + user + assistant messages', () => {
+ const result = chat.convertMessagesToPrompt([
+ new SystemMessage('You are helpful.'),
+ new HumanMessage('Hi'),
+ new AIMessage('Hello!'),
+ new HumanMessage('Bye')
+ ])
+ expect(result).toContain('You are helpful.')
+ expect(result).toContain('User: Hi')
+ expect(result).toContain('Assistant: Hello!')
+ expect(result).toContain('User: Bye')
+ expect(result.endsWith('Assistant:')).toBe(true)
+ })
+
+ it('converts tool messages', () => {
+ const result = chat.convertMessagesToPrompt([
+ new HumanMessage('call tool'),
+ new ToolMessage({ content: '{"result": 42}', tool_call_id: 'tc1' })
+ ])
+ expect(result).toContain('Tool: {"result": 42}')
+ })
+ })
+
+ describe('convertMessagesToOpenAI (OpenAIChatCompletion)', () => {
+ const chat = makeChat('openai-chat-completion')
+
+ it('converts basic messages to OpenAI format', () => {
+ const result = chat.convertMessagesToOpenAI([new SystemMessage('Be helpful'), new HumanMessage('Hello')])
+ expect(result).toEqual([
+ { role: 'system', content: 'Be helpful' },
+ { role: 'user', content: 'Hello' }
+ ])
+ })
+
+ it('converts AI messages with tool_calls', () => {
+ const aiMsg = new AIMessage({
+ content: '',
+ additional_kwargs: {
+ tool_calls: [{ id: 'tc1', type: 'function', function: { name: 'add', arguments: '{"a":1}' } }]
+ }
+ })
+ const result = chat.convertMessagesToOpenAI([aiMsg])
+ expect(result[0].role).toBe('assistant')
+ expect(result[0].tool_calls).toHaveLength(1)
+ })
+
+ it('converts ToolMessage with tool_call_id', () => {
+ const result = chat.convertMessagesToOpenAI([new ToolMessage({ content: 'result', tool_call_id: 'tc1' })])
+ expect(result[0]).toEqual({ role: 'tool', content: 'result', tool_call_id: 'tc1' })
+ })
+ })
+})
+
+// ---------------------------------------------------------------------------
+// BedrockImportedChat — _generate (non-streaming)
+// ---------------------------------------------------------------------------
+describe('BedrockImportedChat _generate', () => {
+ beforeEach(() => {
+ mockSendRuntime.mockReset()
+ })
+
+ it('sends BedrockCompletion request and parses response', async () => {
+ mockSendRuntime.mockResolvedValueOnce(makeInvokeResponse({ completion: 'Hello from Llama!', stop_reason: 'stop' }))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'arn:aws:bedrock:us-east-1:123:imported-model/llama',
+ format: 'bedrock-completion',
+ streaming: false
+ })
+
+ const result = await chat._generate([new HumanMessage('Hi')], {} as any)
+ expect(result.generations).toHaveLength(1)
+ expect(result.generations[0].text).toBe('Hello from Llama!')
+ })
+
+ it('sends OpenAIChatCompletion request and parses response', async () => {
+ mockSendRuntime.mockResolvedValueOnce(
+ makeInvokeResponse({
+ choices: [{ message: { role: 'assistant', content: 'Hello from GPT!' } }],
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
+ })
+ )
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'arn:aws:bedrock:us-east-1:123:imported-model/gpt',
+ format: 'openai-chat-completion',
+ streaming: false
+ })
+
+ const result = await chat._generate([new HumanMessage('Hi')], {} as any)
+ expect(result.generations[0].text).toBe('Hello from GPT!')
+ expect((result.llmOutput as any)?.tokenUsage.totalTokens).toBe(15)
+ })
+
+ it('parses OpenAI tool_calls in response', async () => {
+ mockSendRuntime.mockResolvedValueOnce(
+ makeInvokeResponse({
+ choices: [
+ {
+ message: {
+ role: 'assistant',
+ content: '',
+ tool_calls: [{ id: 'tc1', type: 'function', function: { name: 'calc', arguments: '{"x":1}' } }]
+ }
+ }
+ ]
+ })
+ )
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'openai-chat-completion',
+ streaming: false
+ })
+
+ const result = await chat._generate([new HumanMessage('Use tool')], {} as any)
+ expect((result.generations[0].message.additional_kwargs as any).tool_calls).toHaveLength(1)
+ expect((result.generations[0].message.additional_kwargs as any).tool_calls[0].function.name).toBe('calc')
+ })
+
+ it('handles empty choices in OpenAI response', async () => {
+ mockSendRuntime.mockResolvedValueOnce(makeInvokeResponse({ choices: [] }))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'openai-chat-completion',
+ streaming: false
+ })
+
+ const result = await chat._generate([new HumanMessage('Hi')], {} as any)
+ expect(result.generations[0].text).toBe('')
+ })
+
+ it('normalizes ModelNotReady errors', async () => {
+ mockSendRuntime.mockRejectedValueOnce(new Error('ModelNotReadyException: model is still loading'))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'bedrock-completion',
+ streaming: false
+ })
+
+ await expect(chat._generate([new HumanMessage('Hi')], {} as any)).rejects.toThrow('not ready to serve')
+ })
+
+ it('normalizes ResourceNotFound errors', async () => {
+ mockSendRuntime.mockRejectedValueOnce(new Error('ResourceNotFoundException: model xyz not found'))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'bad-arn',
+ format: 'bedrock-completion',
+ streaming: false
+ })
+
+ await expect(chat._generate([new HumanMessage('Hi')], {} as any)).rejects.toThrow('Model not found')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// BedrockImportedChat — streaming
+// ---------------------------------------------------------------------------
+describe('BedrockImportedChat streaming', () => {
+ beforeEach(() => {
+ mockSendRuntime.mockReset()
+ })
+
+ it('streams BedrockCompletion chunks', async () => {
+ mockSendRuntime.mockResolvedValueOnce(makeStreamResponse([{ completion: 'Hello ' }, { completion: 'world!' }]))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'bedrock-completion',
+ streaming: true
+ })
+
+ const chunks: string[] = []
+ for await (const chunk of chat._streamResponseChunks([new HumanMessage('Hi')], {} as any)) {
+ chunks.push(chunk.text)
+ }
+ expect(chunks).toEqual(['Hello ', 'world!'])
+ })
+
+ it('streams OpenAIChatCompletion delta chunks', async () => {
+ mockSendRuntime.mockResolvedValueOnce(
+ makeStreamResponse([{ choices: [{ delta: { content: 'Hello ' } }] }, { choices: [{ delta: { content: 'world!' } }] }])
+ )
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'openai-chat-completion',
+ streaming: true
+ })
+
+ const chunks: string[] = []
+ for await (const chunk of chat._streamResponseChunks([new HumanMessage('Hi')], {} as any)) {
+ chunks.push(chunk.text)
+ }
+ expect(chunks).toEqual(['Hello ', 'world!'])
+ })
+
+ it('skips empty stream chunks', async () => {
+ mockSendRuntime.mockResolvedValueOnce(makeStreamResponse([{ completion: '' }, { completion: 'data' }, { completion: '' }]))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'bedrock-completion',
+ streaming: true
+ })
+
+ const chunks: string[] = []
+ for await (const chunk of chat._streamResponseChunks([new HumanMessage('Hi')], {} as any)) {
+ chunks.push(chunk.text)
+ }
+ expect(chunks).toEqual(['data'])
+ })
+
+ it('_generate uses streaming when enabled with runManager', async () => {
+ mockSendRuntime.mockResolvedValueOnce(makeStreamResponse([{ completion: 'streamed' }]))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'bedrock-completion',
+ streaming: true
+ })
+
+ const mockRunManager = { handleLLMNewToken: jest.fn() }
+ const result = await chat._generate([new HumanMessage('Hi')], {} as any, mockRunManager as any)
+ expect(result.generations[0].text).toBe('streamed')
+ expect(mockRunManager.handleLLMNewToken).toHaveBeenCalledWith('streamed')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// Image validation
+// ---------------------------------------------------------------------------
+describe('BedrockImportedChat image handling', () => {
+ beforeEach(() => {
+ mockSendRuntime.mockReset()
+ })
+
+ it('rejects remote image URLs', async () => {
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'openai-chat-completion',
+ streaming: false
+ })
+
+ const msg = new HumanMessage({
+ content: [
+ { type: 'text', text: 'What is this?' },
+ { type: 'image_url', image_url: { url: 'https://example.com/image.png' } }
+ ]
+ })
+
+ await expect(chat._generate([msg], {} as any)).rejects.toThrow('base64 data URLs')
+ })
+
+ it('accepts base64 data URL images', async () => {
+ mockSendRuntime.mockResolvedValueOnce(makeInvokeResponse({ choices: [{ message: { content: 'A cat' } }] }))
+
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'openai-chat-completion',
+ streaming: false
+ })
+
+ const msg = new HumanMessage({
+ content: [
+ { type: 'text', text: 'What is this?' },
+ { type: 'image_url', image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' } }
+ ]
+ })
+
+ const result = await chat._generate([msg], {} as any)
+ expect(result.generations[0].text).toBe('A cat')
+ })
+})
+
+// ---------------------------------------------------------------------------
+// IVisionChatModal interface
+// ---------------------------------------------------------------------------
+describe('IVisionChatModal interface', () => {
+ it('implements setMultiModalOption', () => {
+ const chat = new BedrockImportedChat('test-id', {
+ region: 'us-east-1',
+ modelId: 'test-model',
+ format: 'bedrock-completion'
+ })
+ const option = { image: { allowImageUploads: true } }
+ chat.setMultiModalOption(option)
+ expect(chat.multiModalOption).toEqual(option)
+ })
+
+ it('exposes configuredModel and id', () => {
+ const chat = new BedrockImportedChat('my-node-id', {
+ region: 'us-east-1',
+ modelId: 'arn:aws:bedrock:us-east-1:123:imported-model/test',
+ format: 'bedrock-completion'
+ })
+ expect(chat.id).toBe('my-node-id')
+ expect(chat.configuredModel).toBe('arn:aws:bedrock:us-east-1:123:imported-model/test')
+ })
+})
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrock.ts
index 910945308..92480cc20 100644
--- a/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrock.ts
+++ b/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrock.ts
@@ -1,17 +1,106 @@
+/**
+ * Flowise wrapper around LangChain's ChatBedrockConverse.
+ *
+ * Extends the base class with three Flowise-specific behaviors:
+ *
+ * 1. **stopSequences stripping** — Some Bedrock models (DeepSeek, OpenAI GPT OSS)
+ * reject the `stopSequences` inference config field. The `invocationParams()`
+ * override removes it for models flagged with `stop_sequences: false` in models.json.
+ *
+ * 2. **Temperature auto-retry** — Some models (e.g., Claude Opus 4.7) reject the
+ * `temperature` parameter. `_generate()` and `_streamResponseChunks()` catch
+ * "temperature is deprecated" errors and retry without temperature. This avoids
+ * maintaining a per-model flag in models.json since Bedrock has no API to query
+ * supported inference parameters, and the UI form renderer can't conditionally
+ * disable fields based on the selected model.
+ *
+ * 3. **Error normalization** — `_generate()` and `_streamResponseChunks()` catch
+ * Bedrock errors and rewrite them into actionable user-facing messages via
+ * `normalizeBedrockError()` (e.g., "use an inference profile" instead of raw
+ * ValidationException).
+ *
+ * 4. **Multimodal support** — Implements `IVisionChatModal` for image upload handling.
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
+ */
import { IVisionChatModal, IMultiModalOption } from '../../../src'
import { ChatBedrockConverse as LCBedrockChat, ChatBedrockConverseInput } from '@langchain/aws'
+import type { BaseMessage } from '@langchain/core/messages'
+import type { ChatResult } from '@langchain/core/outputs'
+import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager'
+import { normalizeBedrockError } from './utils'
export class BedrockChat extends LCBedrockChat implements IVisionChatModal {
configuredModel: string
configuredMaxToken?: number
multiModalOption: IMultiModalOption
id: string
+ stopSeqUnsupported: Set
- constructor(id: string, fields: ChatBedrockConverseInput) {
+ constructor(id: string, fields: ChatBedrockConverseInput, stopSeqUnsupported?: Set) {
super(fields)
this.id = id
this.configuredModel = fields?.model || ''
this.configuredMaxToken = fields?.maxTokens
+ this.stopSeqUnsupported = stopSeqUnsupported ?? new Set()
+ }
+
+ /**
+ * Strips stopSequences for models that don't support it.
+ * Models are identified by exact ID from models.json (`stop_sequences: false`),
+ * not by provider prefix, to avoid silently stripping from future models that
+ * may add support.
+ */
+ override invocationParams(options?: this['ParsedCallOptions']) {
+ const params = super.invocationParams(options)
+ const modelId = this.model ?? this.configuredModel
+ if (this.stopSeqUnsupported.has(modelId)) {
+ if (params.inferenceConfig) {
+ delete params.inferenceConfig.stopSequences
+ }
+ }
+ return params
+ }
+
+ private isTemperatureDeprecatedError(err: unknown): boolean {
+ return err instanceof Error && err.message.includes('temperature') && err.message.includes('deprecated')
+ }
+
+ private disableTemperature(): void {
+ this.temperature = undefined as any
+ }
+
+ override async _generate(
+ messages: BaseMessage[],
+ options: this['ParsedCallOptions'],
+ runManager?: CallbackManagerForLLMRun
+ ): Promise {
+ try {
+ return await super._generate(messages, options, runManager)
+ } catch (err) {
+ if (this.isTemperatureDeprecatedError(err)) {
+ this.disableTemperature()
+ return await super._generate(messages, options, runManager)
+ }
+ throw normalizeBedrockError(err)
+ }
+ }
+
+ override async *_streamResponseChunks(
+ messages: BaseMessage[],
+ options: this['ParsedCallOptions'],
+ runManager?: CallbackManagerForLLMRun
+ ) {
+ try {
+ yield* super._streamResponseChunks(messages, options, runManager)
+ } catch (err) {
+ if (this.isTemperatureDeprecatedError(err)) {
+ this.disableTemperature()
+ yield* super._streamResponseChunks(messages, options, runManager)
+ return
+ }
+ throw normalizeBedrockError(err)
+ }
}
setMultiModalOption(multiModalOption: IMultiModalOption): void {
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrockImported.ts b/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrockImported.ts
new file mode 100644
index 000000000..c1d740b6b
--- /dev/null
+++ b/packages/components/nodes/chatmodels/AWSBedrock/FlowiseAWSChatBedrockImported.ts
@@ -0,0 +1,447 @@
+import { BaseChatModel, type BaseChatModelParams } from '@langchain/core/language_models/chat_models'
+import { AIMessage, AIMessageChunk, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages'
+import { ChatResult, ChatGenerationChunk } from '@langchain/core/outputs'
+import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager'
+import { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } from '@aws-sdk/client-bedrock-runtime'
+import { IVisionChatModal, IMultiModalOption } from '../../../src'
+
+/**
+ * Request format used when calling Bedrock's InvokeModel API for imported models.
+ *
+ * - 'bedrock-completion': Uses { prompt, max_gen_len, temperature } request shape.
+ * Response: { generation, stop_reason }. For models without a chat template (e.g. GPTBigCode).
+ * - 'openai-chat-completion': Uses { messages, max_tokens, temperature } request shape.
+ * Response: { choices: [{ message }], usage }. For models with chat support (Llama, Qwen, etc.).
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/invoke-imported-model.html
+ */
+export type ImportedModelFormat = 'bedrock-completion' | 'openai-chat-completion'
+
+export interface BedrockImportedChatInput extends BaseChatModelParams {
+ region: string
+ modelId: string
+ format: ImportedModelFormat
+ temperature?: number
+ maxTokens?: number
+ streaming?: boolean
+ credentials?: {
+ accessKeyId: string
+ secretAccessKey: string
+ sessionToken?: string
+ }
+}
+
+interface OpenAIToolCall {
+ id: string
+ type: 'function'
+ function: { name: string; arguments: string }
+}
+
+interface ImportedModelInfo {
+ instructSupported?: boolean
+ modelArchitecture?: string
+ supportedFormats?: string[]
+}
+
+const _modelInfoCache = new Map()
+
+/**
+ * Fetches metadata and supported request formats for an imported Bedrock model.
+ *
+ * Two-step process:
+ * 1. Calls GetImportedModel to retrieve instructSupported and modelArchitecture.
+ * 2. Probes InvokeModel with an empty body — the resulting ValidationException
+ * lists the formats the model accepts (e.g. "ChatCompletionRequest, BedrockMetaCompletionRequest").
+ *
+ * Results are cached per modelId for the process lifetime.
+ * Errors propagate directly — if the model doesn't exist, the caller gets the real Bedrock error.
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GetImportedModel.html
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/invoke-imported-model.html
+ */
+export async function getImportedModelInfo(
+ modelId: string,
+ region: string,
+ credentials?: { accessKeyId: string; secretAccessKey: string; sessionToken?: string }
+): Promise {
+ if (_modelInfoCache.has(modelId)) {
+ return _modelInfoCache.get(modelId)!
+ }
+
+ const info: ImportedModelInfo = {}
+
+ // Step 1: GetImportedModel for metadata (instructSupported, modelArchitecture)
+ const { BedrockClient, GetImportedModelCommand } = await import('@aws-sdk/client-bedrock')
+ const client = new BedrockClient({
+ region,
+ ...(credentials && { credentials })
+ })
+ const resp = await client.send(new GetImportedModelCommand({ modelIdentifier: modelId }))
+ info.instructSupported = resp.instructSupported
+ info.modelArchitecture = resp.modelArchitecture
+
+ // Step 2: Probe InvokeModel with an empty body to discover supported formats.
+ // Bedrock returns a ValidationException whose message lists the formats, e.g.:
+ // "Available for this model: ChatCompletionRequest, BedrockMetaCompletionRequest, ..."
+ // This is a control-plane call that fails instantly — no inference cost.
+ try {
+ const runtimeClient = new BedrockRuntimeClient({
+ region,
+ ...(credentials && { credentials })
+ })
+ await runtimeClient.send(
+ new InvokeModelCommand({
+ modelId,
+ contentType: 'application/json',
+ accept: 'application/json',
+ body: JSON.stringify({})
+ })
+ )
+ } catch (probeErr: any) {
+ const msg = probeErr?.message ?? ''
+ const match = msg.match(/Available for this model:\s*(.+?)(?:"|$)/)
+ if (match) {
+ info.supportedFormats = match[1].split(',').map((s: string) => s.trim().toLowerCase())
+ } else if (msg.includes('prompt') && msg.includes('must be provided')) {
+ // Model expects a prompt field (completion-style, no chat template)
+ info.supportedFormats = ['completionrequest']
+ } else {
+ throw probeErr
+ }
+ }
+
+ _modelInfoCache.set(modelId, info)
+ return info
+}
+
+/**
+ * Selects the best request format based on the model's supported formats.
+ * Prefers OpenAIChatCompletion (structured messages, tool_calls, token usage)
+ * over BedrockCompletion (raw prompt string). Falls back to openai-chat-completion
+ * if no format info is available.
+ */
+export function detectFormat(supportedFormats?: string[]): ImportedModelFormat {
+ if (!supportedFormats?.length) {
+ return 'openai-chat-completion'
+ }
+
+ // Prefer OpenAI Chat if available — structured messages, tool_calls, token usage
+ if (supportedFormats.some((f) => f.includes('chatcompletion'))) {
+ return 'openai-chat-completion'
+ }
+
+ return 'bedrock-completion'
+}
+
+export function _resetModelInfoCache(): void {
+ _modelInfoCache.clear()
+}
+
+/**
+ * LangChain-compatible chat model for Bedrock imported models that don't support
+ * the Converse API (instructSupported: false).
+ *
+ * Uses InvokeModel / InvokeModelWithResponseStream instead of Converse / ConverseStream.
+ * Supports two request formats (auto-detected at init time via getImportedModelInfo):
+ * - OpenAIChatCompletion: messages array with tool_calls support
+ * - BedrockCompletion: raw prompt string with max_gen_len
+ *
+ * This class is instantiated by AWSChatBedrock.init() when it detects an imported-model
+ * ARN in the Custom Model ARN field. Users don't interact with it directly.
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/invoke-imported-model.html
+ * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html
+ */
+export class BedrockImportedChat extends BaseChatModel implements IVisionChatModal {
+ configuredModel: string
+ configuredMaxToken?: number
+ multiModalOption: IMultiModalOption
+ id: string
+
+ private region: string
+ private modelId: string
+ private format: ImportedModelFormat
+ private temperature: number
+ private maxTokens: number
+ private streamingEnabled: boolean
+ private credentials?: { accessKeyId: string; secretAccessKey: string; sessionToken?: string }
+ private client: BedrockRuntimeClient
+
+ constructor(id: string, fields: BedrockImportedChatInput) {
+ super(fields)
+ this.id = id
+ this.region = fields.region
+ this.modelId = fields.modelId
+ this.format = fields.format
+ this.temperature = fields.temperature ?? 0.7
+ this.maxTokens = fields.maxTokens ?? 200
+ this.streamingEnabled = fields.streaming ?? true
+ this.credentials = fields.credentials
+ this.configuredModel = fields.modelId
+ this.configuredMaxToken = fields.maxTokens
+
+ this.client = new BedrockRuntimeClient({
+ region: this.region,
+ ...(this.credentials && { credentials: this.credentials })
+ })
+ }
+
+ _llmType(): string {
+ return 'bedrock-imported'
+ }
+
+ get callKeys(): string[] {
+ return ['stop', 'signal', 'options']
+ }
+
+ setMultiModalOption(multiModalOption: IMultiModalOption): void {
+ this.multiModalOption = multiModalOption
+ }
+
+ async _generate(
+ messages: BaseMessage[],
+ options: this['ParsedCallOptions'],
+ runManager?: CallbackManagerForLLMRun
+ ): Promise {
+ if (this.streamingEnabled && runManager) {
+ const generations: ChatGenerationChunk[] = []
+ let fullText = ''
+ for await (const chunk of this._streamResponseChunks(messages, options, runManager)) {
+ generations.push(chunk)
+ fullText += chunk.text
+ }
+ return {
+ generations: [{ text: fullText, message: new AIMessage(fullText) }]
+ }
+ }
+
+ const body = this.buildRequestBody(messages)
+ try {
+ const resp = await this.client.send(
+ new InvokeModelCommand({
+ modelId: this.modelId,
+ contentType: 'application/json',
+ accept: 'application/json',
+ body: JSON.stringify(body)
+ })
+ )
+ if (resp.body == null) throw new Error('Response body is undefined')
+ const decoded = JSON.parse(new TextDecoder().decode(resp.body))
+ return this.parseResponse(decoded)
+ } catch (err) {
+ throw this.normalizeError(err)
+ }
+ }
+
+ async *_streamResponseChunks(
+ messages: BaseMessage[],
+ _options: this['ParsedCallOptions'],
+ runManager?: CallbackManagerForLLMRun
+ ): AsyncGenerator {
+ const body = this.buildRequestBody(messages)
+ try {
+ const resp = await this.client.send(
+ new InvokeModelWithResponseStreamCommand({
+ modelId: this.modelId,
+ contentType: 'application/json',
+ accept: 'application/json',
+ body: JSON.stringify(body)
+ })
+ )
+
+ if (!resp.body) {
+ throw new Error('No response stream received from Bedrock')
+ }
+
+ for await (const event of resp.body) {
+ if (event.chunk?.bytes) {
+ const decoded = JSON.parse(new TextDecoder().decode(event.chunk.bytes))
+ const text = this.extractStreamChunkText(decoded)
+ if (text) {
+ const chunk = new ChatGenerationChunk({
+ text,
+ message: new AIMessageChunk(text)
+ })
+ yield chunk
+ await runManager?.handleLLMNewToken(text)
+ }
+ }
+ }
+ } catch (err) {
+ throw this.normalizeError(err)
+ }
+ }
+
+ private buildRequestBody(messages: BaseMessage[]): Record {
+ if (this.format === 'openai-chat-completion') {
+ return this.buildOpenAIChatBody(messages)
+ }
+ return this.buildBedrockCompletionBody(messages)
+ }
+
+ private buildBedrockCompletionBody(messages: BaseMessage[]): Record {
+ const prompt = this.convertMessagesToPrompt(messages)
+ return {
+ prompt,
+ temperature: this.temperature,
+ max_gen_len: this.maxTokens
+ }
+ }
+
+ private buildOpenAIChatBody(messages: BaseMessage[]): Record {
+ const openaiMessages = this.convertMessagesToOpenAI(messages)
+ return {
+ messages: openaiMessages,
+ temperature: this.temperature,
+ max_tokens: this.maxTokens
+ }
+ }
+
+ convertMessagesToPrompt(messages: BaseMessage[]): string {
+ const parts: string[] = []
+ for (const msg of messages) {
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
+ if (msg instanceof SystemMessage) {
+ parts.push(`${content}\n`)
+ } else if (msg instanceof HumanMessage) {
+ parts.push(`User: ${content}`)
+ } else if (msg instanceof AIMessage) {
+ parts.push(`Assistant: ${content}`)
+ } else if (msg instanceof ToolMessage) {
+ parts.push(`Tool: ${content}`)
+ } else {
+ parts.push(content)
+ }
+ }
+ parts.push('Assistant:')
+ return parts.join('\n')
+ }
+
+ convertMessagesToOpenAI(messages: BaseMessage[]): Array> {
+ return messages.map((msg) => {
+ const content = typeof msg.content === 'string' ? msg.content : this.convertMultiModalContent(msg.content)
+ if (msg instanceof SystemMessage) {
+ return { role: 'system', content }
+ }
+ if (msg instanceof HumanMessage) {
+ return { role: 'user', content }
+ }
+ if (msg instanceof AIMessage) {
+ const result: Record = { role: 'assistant', content }
+ if (msg.additional_kwargs?.tool_calls) {
+ result.tool_calls = msg.additional_kwargs.tool_calls
+ }
+ return result
+ }
+ if (msg instanceof ToolMessage) {
+ return {
+ role: 'tool',
+ content,
+ tool_call_id: (msg as ToolMessage).tool_call_id
+ }
+ }
+ return { role: 'user', content }
+ })
+ }
+
+ private convertMultiModalContent(content: unknown): unknown {
+ if (!Array.isArray(content)) return content
+ return content.map((part: any) => {
+ if (part.type === 'image_url' && typeof part.image_url?.url === 'string') {
+ const url = part.image_url.url
+ if (!url.startsWith('data:')) {
+ throw new Error(
+ 'AWS Bedrock Imported models only support base64 data URLs for images, ' +
+ 'not remote URLs. Convert the image to a data URL first.'
+ )
+ }
+ return part
+ }
+ return part
+ })
+ }
+
+ private parseResponse(decoded: Record): ChatResult {
+ if (this.format === 'openai-chat-completion') {
+ return this.parseOpenAIChatResponse(decoded)
+ }
+ return this.parseBedrockCompletionResponse(decoded)
+ }
+
+ private parseBedrockCompletionResponse(decoded: Record): ChatResult {
+ const text = (decoded.completion as string) ?? (decoded.generation as string) ?? ''
+ return {
+ generations: [{ text, message: new AIMessage(text) }]
+ }
+ }
+
+ private parseOpenAIChatResponse(decoded: Record): ChatResult {
+ const choices = decoded.choices as Array> | undefined
+ if (!choices?.length) {
+ return { generations: [{ text: '', message: new AIMessage('') }] }
+ }
+ const choice = choices[0]
+ const message = choice.message ?? {}
+ const text = (message.content as string) ?? ''
+ const toolCalls: OpenAIToolCall[] | undefined = message.tool_calls
+
+ const aiMsg = new AIMessage({
+ content: text,
+ additional_kwargs: toolCalls ? { tool_calls: toolCalls } : {}
+ })
+
+ return {
+ generations: [{ text, message: aiMsg }],
+ llmOutput: decoded.usage
+ ? {
+ tokenUsage: {
+ promptTokens: (decoded.usage as any).prompt_tokens,
+ completionTokens: (decoded.usage as any).completion_tokens,
+ totalTokens: (decoded.usage as any).total_tokens
+ }
+ }
+ : undefined
+ }
+ }
+
+ private extractStreamChunkText(decoded: Record): string {
+ if (this.format === 'openai-chat-completion') {
+ const choices = decoded.choices as Array> | undefined
+ if (!choices?.length) return ''
+ const delta = choices[0].delta ?? {}
+ return (delta.content as string) ?? ''
+ }
+ return (decoded.completion as string) ?? (decoded.generation as string) ?? (decoded.outputText as string) ?? ''
+ }
+
+ private normalizeError(err: unknown): Error {
+ if (!(err instanceof Error)) return err as Error
+ const msg = err.message ?? ''
+
+ if (msg.includes('ModelNotReadyException') || msg.includes('not ready')) {
+ return new Error(
+ `The imported model is not ready to serve requests. ` +
+ `This can happen shortly after import or if the model is being updated. ` +
+ `Wait a few minutes and try again. Original error: ${msg}`
+ )
+ }
+
+ if (msg.includes('ValidationException') && msg.includes('format')) {
+ return new Error(
+ `The request format may not match what this imported model expects. ` +
+ `The auto-detected format is "${this.format}". ` +
+ `Original error: ${msg}`
+ )
+ }
+
+ if (msg.includes('ResourceNotFoundException')) {
+ return new Error(
+ `Model not found: "${this.modelId}". ` +
+ `Verify the model ARN or ID is correct and the model exists in the "${this.region}" region. ` +
+ `Original error: ${msg}`
+ )
+ }
+
+ return err
+ }
+}
diff --git a/packages/components/nodes/chatmodels/AWSBedrock/utils.ts b/packages/components/nodes/chatmodels/AWSBedrock/utils.ts
new file mode 100644
index 000000000..1d5660ead
--- /dev/null
+++ b/packages/components/nodes/chatmodels/AWSBedrock/utils.ts
@@ -0,0 +1,406 @@
+/**
+ * Bedrock-specific utilities for the AWS Bedrock Converse node.
+ *
+ * This module centralizes all Bedrock model resolution logic:
+ *
+ * - **validateEndpointHost** — Sanitizes the endpointHost field. Auto-migrates
+ * ARNs that users mistakenly placed there (the original "invalid URL" bug).
+ *
+ * - **discoverInferenceProfiles** — Runtime discovery of which inference profiles
+ * exist in a given region via the Bedrock control-plane API. Cached per region.
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles.html
+ *
+ * - **resolveBedrockModel** — Determines the correct model ID and optional
+ * applicationInferenceProfile for a Converse API call. Handles ARNs,
+ * geo-prefixed profile IDs, and auto-application of profiles for models
+ * that require them.
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html
+ *
+ * - **getStopSeqUnsupportedModels** — Loads the set of models that reject the
+ * stopSequences inference config field, read from models.json (`stop_sequences: false`).
+ *
+ * - **normalizeBedrockError** — Rewrites raw Bedrock Converse API errors into
+ * actionable user-facing messages.
+ *
+ * All model metadata (inference profile geos, stop_sequences flags) lives in
+ * models.json as the single source of truth, loaded via the model loader.
+ */
+
+// ---------------------------------------------------------------------------
+// Endpoint-host validation
+// ---------------------------------------------------------------------------
+
+export interface EndpointHostResult {
+ /** The sanitized hostname to use, or undefined if the value was an ARN/URL that should be ignored. */
+ hostname?: string
+ /** If the value looked like an ARN, it is returned here so init() can treat it as a model target. */
+ migratedArn?: string
+}
+
+/**
+ * Inspects the endpointHost value and decides what to do with it.
+ *
+ * - Bare hostname (valid) → returned as-is.
+ * - ARN (common misconfiguration) → stripped from endpointHost and
+ * returned as `migratedArn` so init() can route it to the inference
+ * profile path. This provides backward compatibility for existing
+ * flows that placed ARNs in the wrong field.
+ * - URL with scheme → hostname extracted and returned with a console
+ * warning.
+ */
+export function validateEndpointHost(value: string): EndpointHostResult {
+ // ARN in endpointHost — auto-migrate to inference profile
+ if (value.startsWith('arn:aws:bedrock:')) {
+ console.warn(
+ `[AWSBedrock] "Custom Endpoint Host" contained an ARN (${value}). ` +
+ `This has been auto-migrated to the inference profile path. ` +
+ `Please move this value to the "Custom Model ARN" field instead.`
+ )
+ return { migratedArn: value }
+ }
+
+ // URL with scheme — extract hostname
+ if (value.includes('://')) {
+ try {
+ const url = new URL(value)
+ console.warn(`[AWSBedrock] "Custom Endpoint Host" contained a full URL. ` + `Using hostname "${url.hostname}" only.`)
+ return { hostname: url.hostname }
+ } catch {
+ // If URL parsing fails, reject
+ return {}
+ }
+ }
+
+ // Contains a path — likely a mistake, but not an ARN
+ if (value.includes('/')) {
+ console.warn(
+ `[AWSBedrock] "Custom Endpoint Host" contained a path separator. ` + `Using the value before the first "/" as hostname.`
+ )
+ return { hostname: value.split('/')[0] }
+ }
+
+ // Valid bare hostname
+ return { hostname: value }
+}
+
+// ---------------------------------------------------------------------------
+// Runtime inference-profile discovery
+// ---------------------------------------------------------------------------
+
+/**
+ * Per-region cache of available inference profile IDs.
+ * Populated by calling `ListInferenceProfiles` once per region.
+ */
+const _regionProfileCache = new Map>()
+
+/**
+ * Discovers which system-defined inference profiles are available in a
+ * given region by calling the Bedrock control-plane API. Results are
+ * cached in-memory for the process lifetime.
+ *
+ * This is necessary because inference profile availability is per-region,
+ * not per-geo. For example, `eu.amazon.nova-pro-v1:0` exists in eu-west-1
+ * but NOT in eu-west-2, even though both are EU regions.
+ *
+ * Uses the same credentials the user configured on the Bedrock node
+ * (UI creds, AssumeRole, or SDK default chain).
+ *
+ * Note: The API paginates (~13 results per page despite maxResults: 100),
+ * so we loop on nextToken to get the full list.
+ *
+ * On any error (auth failure, region not supported, etc.) returns an
+ * empty set — the caller falls back to direct invocation without a profile.
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_ListInferenceProfiles.html
+ */
+export async function discoverInferenceProfiles(
+ region: string,
+ credentials?: { accessKeyId: string; secretAccessKey: string; sessionToken?: string }
+): Promise> {
+ if (_regionProfileCache.has(region)) {
+ return _regionProfileCache.get(region)!
+ }
+
+ try {
+ const { BedrockClient, ListInferenceProfilesCommand } = await import('@aws-sdk/client-bedrock')
+ const client = new BedrockClient({
+ region,
+ ...(credentials && { credentials })
+ })
+ const ids = new Set()
+ let nextToken: string | undefined
+ do {
+ const resp = await client.send(
+ new ListInferenceProfilesCommand({
+ typeEquals: 'SYSTEM_DEFINED',
+ maxResults: 100,
+ ...(nextToken && { nextToken })
+ })
+ )
+ for (const p of resp.inferenceProfileSummaries ?? []) {
+ if (p.inferenceProfileId) {
+ ids.add(p.inferenceProfileId)
+ }
+ }
+ nextToken = resp.nextToken
+ } while (nextToken)
+ _regionProfileCache.set(region, ids)
+ return ids
+ } catch {
+ const empty = new Set()
+ _regionProfileCache.set(region, empty)
+ return empty
+ }
+}
+
+/** Exported for testing — resets the region profile cache. */
+export function _resetRegionProfileCache(): void {
+ _regionProfileCache.clear()
+}
+
+// ---------------------------------------------------------------------------
+// Stop-sequence support
+// ---------------------------------------------------------------------------
+
+/**
+ * Cached set of model IDs that do NOT support the stopSequences
+ * inference-config field. Populated from models.json via the model
+ * loader by looking for `stop_sequences: false`.
+ *
+ * Affected models include DeepSeek and OpenAI GPT OSS variants which
+ * return: "This model doesn't support the stopSequences field."
+ * We use exact model IDs (not prefix matching) because future models
+ * from the same provider may add stopSequences support.
+ *
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html
+ */
+let _stopSeqUnsupported: Set | null = null
+
+export async function getStopSeqUnsupportedModels(): Promise> {
+ if (_stopSeqUnsupported) return _stopSeqUnsupported
+ try {
+ const { getModels, MODEL_TYPE } = await import('../../../src/modelLoader')
+ const models = await getModels(MODEL_TYPE.CHAT, 'awsChatBedrock')
+ _stopSeqUnsupported = new Set(models.filter((m: any) => m.stop_sequences === false).map((m: any) => m.name))
+ } catch {
+ _stopSeqUnsupported = new Set()
+ }
+ return _stopSeqUnsupported
+}
+
+/** Exported for testing — resets the stop-sequence cache. */
+export function _resetStopSeqCache(): void {
+ _stopSeqUnsupported = null
+}
+
+// ---------------------------------------------------------------------------
+// Model / inference-profile resolution
+// ---------------------------------------------------------------------------
+
+/**
+ * Cached map of model ID → available geo prefixes (e.g. ['us','eu','global']).
+ * Populated at first use from the model catalog (models.json via the
+ * model loader) by reading the `inference_profile_geos` array on each model.
+ */
+let _inferenceProfileGeos: Map | null = null
+
+async function getInferenceProfileGeos(): Promise