Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ COPY ./backend ${APP_HOME}
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --extra cpu

# Install Amazon Bedrock SDK separately with --no-deps to avoid the langchain-aws
# transitive numpy<2 constraint clashing with the pinned numpy==2.3.5.
# ChatBedrockConverse only needs boto3 + langchain-core (already present) at runtime.
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install "boto3>=1.34.0" && \
uv pip install "langchain-aws>=0.2.0,<0.3.0" --no-deps

# Build g2-ssr
FROM registry.cn-qingdao.aliyuncs.com/dataease/sqlbot-base:latest AS ssr-builder

Expand Down
99 changes: 98 additions & 1 deletion backend/apps/ai_model/model_factory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from functools import lru_cache
import json
import re
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, Type

Expand All @@ -18,6 +19,36 @@

# from langchain_community.llms import Tongyi, VLLM

# protocol(int) stored on the model record -> internal model_type used by the factory
PROTOCOL_OPENAI = 1
PROTOCOL_VLLM = 2
PROTOCOL_BEDROCK = 3

_PROTOCOL_MODEL_TYPE: Dict[int, str] = {
PROTOCOL_OPENAI: "openai",
PROTOCOL_VLLM: "vllm",
PROTOCOL_BEDROCK: "bedrock",
}


def get_model_type_by_protocol(protocol: Optional[int]) -> str:
"""Map the stored protocol value to the factory model_type.

Falls back to the legacy behaviour (1 -> openai, everything else -> vllm)
for any unknown value so existing records keep working.
"""
if protocol == PROTOCOL_OPENAI:
return "openai"
return _PROTOCOL_MODEL_TYPE.get(protocol, "vllm")


def _region_from_bedrock_url(url: Optional[str]) -> Optional[str]:
"""Extract the AWS region from a bedrock-runtime / bedrock-mantle endpoint url."""
if not url:
return None
match = re.search(r"bedrock(?:-runtime|-mantle)?\.([a-z0-9-]+)\.(?:amazonaws\.com|api\.aws)", url)
return match.group(1) if match else None

class LLMConfig(BaseModel):
"""Base configuration class for large language models"""
model_id: Optional[int] = None
Expand Down Expand Up @@ -109,6 +140,71 @@ def generate(self, prompt: str) -> str:
return self.llm.invoke(prompt)


class BedrockLLM(BaseLLM):
"""Amazon Bedrock support for both available endpoint families:

- ``bedrock-runtime`` (default): native Bedrock inference via the Converse
API. Authenticates with SigV4 using the standard AWS credential chain
(IAM role / profile / env vars) or explicit keys passed as advanced args.
- ``bedrock-mantle``: OpenAI-compatible endpoint (Chat Completions). Reuses
the OpenAI client with the mantle base url and a Bedrock API key.

Control args are read from ``additional_params``:
- ``endpoint_type``: ``runtime`` (default) or ``mantle``
- ``region_name``: e.g. ``us-east-1`` (inferred from the url when omitted)
- ``aws_access_key_id`` / ``aws_secret_access_key`` / ``aws_session_token``
- ``provider``: optional explicit Bedrock provider for the model
Any remaining params (``temperature``, ``max_tokens`` ...) are forwarded to
the underlying model.
"""

def _init_llm(self) -> BaseChatModel:
params = dict(self.config.additional_params or {})
endpoint_type = str(params.pop("endpoint_type", "") or "runtime").lower()
region_name = params.pop("region_name", None) or _region_from_bedrock_url(self.config.api_base_url)

if endpoint_type == "mantle":
# OpenAI-compatible endpoint, served by the bedrock-mantle endpoint.
base_url = self.config.api_base_url
if not base_url and region_name:
base_url = f"https://bedrock-mantle.{region_name}.api.aws/v1"
return BaseChatOpenAI(
model=self.config.model_name,
api_key=self.config.api_key or 'Empty',
base_url=base_url,
stream_usage=True,
**params,
)

# default: native bedrock-runtime endpoint via the Converse API
from langchain_aws import ChatBedrockConverse

aws_access_key_id = params.pop("aws_access_key_id", None)
aws_secret_access_key = params.pop("aws_secret_access_key", None)
aws_session_token = params.pop("aws_session_token", None)
provider = params.pop("provider", None)

kwargs: Dict[str, Any] = {}
if region_name:
kwargs["region_name"] = region_name
# api_domain may hold a custom/private bedrock-runtime endpoint url
if self.config.api_base_url and "bedrock-mantle" not in self.config.api_base_url:
kwargs["endpoint_url"] = self.config.api_base_url
if aws_access_key_id and aws_secret_access_key:
kwargs["aws_access_key_id"] = aws_access_key_id
kwargs["aws_secret_access_key"] = aws_secret_access_key
if aws_session_token:
kwargs["aws_session_token"] = aws_session_token
if provider:
kwargs["provider"] = provider

return ChatBedrockConverse(
model=self.config.model_name,
**kwargs,
**params,
)


class LLMFactory:
"""Large Language Model Factory Class"""

Expand All @@ -117,6 +213,7 @@ class LLMFactory:
"tongyi": OpenAILLM,
"vllm": OpenAIvLLM,
"azure": OpenAIAzureLLM,
"bedrock": BedrockLLM,
}

@classmethod
Expand Down Expand Up @@ -173,7 +270,7 @@ async def get_default_config(custom_model_id: Optional[int] = None) -> LLMConfig
# ๆž„้€  LLMConfig
return LLMConfig(
model_id=db_model.id,
model_type="openai" if db_model.protocol == 1 else "vllm",
model_type=get_model_type_by_protocol(db_model.protocol),
model_name=db_model.base_model,
api_key=db_model.api_key,
api_base_url=db_model.api_domain,
Expand Down
96 changes: 93 additions & 3 deletions backend/apps/system/api/aimodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from fastapi.responses import StreamingResponse
from sqlmodel import func, select, update, delete

from apps.ai_model.model_factory import LLMConfig, LLMFactory
from apps.ai_model.model_factory import LLMConfig, LLMFactory, get_model_type_by_protocol
from apps.swagger.i18n import PLACEHOLDER_PREFIX
from apps.system.crud.aimodel_manage import get_ai_model_list_by_workspace
from apps.system.models.system_model import AiModelDetail, AiModelWorkspaceMapping, AiModelBrief
from apps.system.schemas.ai_model_schema import AiModelConfigItem, AiModelCreator, AiModelEditor, AiModelGridItem
from apps.system.schemas.ai_model_schema import AiModelConfigItem, AiModelCreator, AiModelEditor, AiModelGridItem, \
BedrockModelListReq
from apps.system.schemas.permission import SqlbotPermission, require_permissions
from common.core.deps import SessionDep, Trans, CurrentUser
from common.utils.crypto import sqlbot_decrypt
Expand All @@ -29,7 +30,7 @@ async def generate():
additional_params = {item.key: prepare_model_arg(item.val) for item in info.config_list if
item.key and item.val}
config = LLMConfig(
model_type="openai" if info.protocol == 1 else "vllm",
model_type=get_model_type_by_protocol(info.protocol),
model_name=info.base_model,
api_key=info.api_key,
api_base_url=info.api_domain,
Expand All @@ -51,6 +52,95 @@ async def generate():
return StreamingResponse(generate(), media_type="application/x-ndjson")


# Cross-region "geo" inference profile prefixes (route across multiple countries)
_BEDROCK_GEO_PREFIXES = {"us", "eu", "apac"}


@router.post("/bedrock/models", include_in_schema=False)
@require_permissions(permission=SqlbotPermission(role=['admin']))
async def list_bedrock_models(info: BedrockModelListReq, trans: Trans):
"""List invocable Amazon Bedrock models for the given region, grouped by scope.

Groups: In-Region (single country/region profiles), Cross-Region (Geo)
(us/eu/apac multi-country profiles), Global, and direct on-demand
Foundation Models. Uses the standard AWS credential chain (instance role)
unless explicit keys are provided.
"""
try:
import boto3
except ImportError:
raise Exception("boto3 is not installed; Amazon Bedrock support is unavailable")

client_kwargs = {"region_name": info.region_name}
if info.aws_access_key_id and info.aws_secret_access_key:
client_kwargs["aws_access_key_id"] = info.aws_access_key_id
client_kwargs["aws_secret_access_key"] = info.aws_secret_access_key
if info.aws_session_token:
client_kwargs["aws_session_token"] = info.aws_session_token

try:
client = boto3.client("bedrock", **client_kwargs)

groups: dict[str, list] = {
"In-Region": [],
"Cross-Region (Geo)": [],
"Global": [],
"Foundation Models (On-demand)": [],
}

# 1) system-defined inference profiles (the canonical invocable ids)
next_token = None
while True:
kwargs = {"typeEquals": "SYSTEM_DEFINED", "maxResults": 100}
if next_token:
kwargs["nextToken"] = next_token
resp = client.list_inference_profiles(**kwargs)
for p in resp.get("inferenceProfileSummaries", []):
if p.get("status") != "ACTIVE":
continue
pid = p.get("inferenceProfileId")
if not pid:
continue
prefix = pid.split(".")[0]
if prefix == "global":
grp = "Global"
elif prefix in _BEDROCK_GEO_PREFIXES:
grp = "Cross-Region (Geo)"
else:
grp = "In-Region"
name = p.get("inferenceProfileName") or pid
groups[grp].append({"label": f"{name} ({pid})", "value": pid})
next_token = resp.get("nextToken")
if not next_token:
break

# 2) directly-invokable on-demand text foundation models
try:
fm = client.list_foundation_models(byOutputModality="TEXT", byInferenceType="ON_DEMAND")
for m in fm.get("modelSummaries", []):
if m.get("modelLifecycle", {}).get("status") != "ACTIVE":
continue
mid = m.get("modelId")
if not mid:
continue
name = m.get("modelName") or mid
groups["Foundation Models (On-demand)"].append({"label": f"{name} ({mid})", "value": mid})
except Exception as e:
SQLBotLogUtil.warning(f"list_foundation_models failed: {e}")

except Exception as e:
SQLBotLogUtil.error(f"Error listing Bedrock models: {e}")
raise Exception(str(e))

order = ["In-Region", "Cross-Region (Geo)", "Global", "Foundation Models (On-demand)"]
result = []
for g in order:
opts = sorted(groups[g], key=lambda x: x["value"])
if opts:
result.append({"label": g, "options": opts})
return result


@router.get("/default", include_in_schema=False)
async def check_default(session: SessionDep, trans: Trans):
db_model = session.exec(
Expand Down
9 changes: 8 additions & 1 deletion backend/apps/system/schemas/ai_model_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ class AiModelCreator(AiModelItem):
config_list: List[AiModelConfigItem] = Field(description=f"{PLACEHOLDER_PREFIX}config_list")

class AiModelEditor(AiModelCreator, BaseCreatorDTO):
pass
pass

class BedrockModelListReq(BaseModel):
region_name: str = Field(default="us-east-1", description="AWS region, e.g. ap-northeast-1")
endpoint_type: str = Field(default="runtime", description="runtime or mantle")
aws_access_key_id: str | None = Field(default=None, description="optional explicit AK")
aws_secret_access_key: str | None = Field(default=None, description="optional explicit SK")
aws_session_token: str | None = Field(default=None, description="optional session token")
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ explicit = true

[[tool.uv.index]]
name = "default"
url = "http://mirrors.aliyun.com/pypi/simple"
url = "https://pypi.org/simple"
default = true

[[tool.uv.index]]
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export const modelApi = {
request.post(`/system/platform/org/${id}`, { lazy, pid }),
userSync: (data: any) => request.post(`/system/platform/user/sync`, data),
list_by_ws: () => request.get(`/system/aimodel/list/by_ws`),
bedrockModels: (data: any) => request.post('/system/aimodel/bedrock/models', data),
}
39 changes: 39 additions & 0 deletions frontend/src/entity/supplier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,45 @@ export const supplierList: Array<{
},
},
},
{
id: 16,
name: 'Amazon Bedrock',
i18nKey: 'supplier.aws_bedrock',
icon: icon_common_openai,
type: 'bedrock',
is_private: true,
model_config: {
0: {
api_domain: 'https://bedrock-runtime.us-east-1.amazonaws.com',
common_args: [
{ key: 'endpoint_type', val: 'runtime', type: 'string' },
{ key: 'region_name', val: 'us-east-1', type: 'string' },
{ key: 'temperature', val: 0.6, type: 'number', range: '[0, 1]' },
],
// model_options are fetched dynamically via the Bedrock list API
model_options: [],
},
},
},
{
id: 17,
name: 'Amazon Bedrock (OpenAI ๅ…ผๅฎน)',
i18nKey: 'supplier.aws_bedrock_mantle',
icon: icon_common_openai,
type: 'bedrock',
model_config: {
0: {
api_domain: 'https://bedrock-mantle.us-east-1.api.aws/v1',
common_args: [
{ key: 'endpoint_type', val: 'mantle', type: 'string' },
{ key: 'region_name', val: 'us-east-1', type: 'string' },
{ key: 'temperature', val: 0.6, type: 'number', range: '[0, 1]' },
],
// model_options are fetched dynamically via the Bedrock list API
model_options: [],
},
},
},
]

export const base_model_options = (supplier_id: number, model_type?: number) => {
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@
"model_name": "Model name",
"custom_model_name": "Custom model name",
"enter_to_add": "For models not listed in the list, just enter the model name and press Enter to add",
"refresh_models": "Refresh model list",
"bedrock_no_models": "No invocable Bedrock models found for this region. Check region and model access.",
"bedrock_list_failed": "Failed to list Bedrock models. Falling back to manual input.",
"api_domain_name": "API domain name",
"advanced_settings": "Advanced settings",
"model_parameters": "Model parameters",
Expand Down Expand Up @@ -947,7 +950,9 @@
"tencent_cloud": "Tencent Cloud",
"volcano_engine": "Volcano Engine",
"minimax": "MiniMax",
"generic_openai": "Generic OpenAI"
"generic_openai": "Generic OpenAI",
"aws_bedrock": "Amazon Bedrock",
"aws_bedrock_mantle": "Amazon Bedrock (OpenAI-compatible)"
},
"modelType": {
"llm": "Large Language Model"
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/i18n/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@
"model_name": "๋ชจ๋ธ ์ด๋ฆ„",
"custom_model_name": "์‚ฌ์šฉ์ž ์ •์˜ ๋ชจ๋ธ ์ด๋ฆ„",
"enter_to_add": "๋ชฉ๋ก์— ์—†๋Š” ๋ชจ๋ธ์€ ๋ชจ๋ธ ์ด๋ฆ„์„ ์ง์ ‘ ์ž…๋ ฅํ•œ ํ›„ Enter ํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค",
"refresh_models": "๋ชจ๋ธ ๋ชฉ๋ก ์ƒˆ๋กœ ๊ณ ์นจ",
"bedrock_no_models": "์ด ๋ฆฌ์ „์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•œ Bedrock ๋ชจ๋ธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฆฌ์ „๊ณผ ๋ชจ๋ธ ์•ก์„ธ์Šค ๊ถŒํ•œ์„ ํ™•์ธํ•˜์„ธ์š”.",
"bedrock_list_failed": "Bedrock ๋ชจ๋ธ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ ID๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•˜์„ธ์š”.",
"api_domain_name": "API ๋„๋ฉ”์ธ๋ช…",
"advanced_settings": "๊ณ ๊ธ‰ ์„ค์ •",
"model_parameters": "๋ชจ๋ธ ๋งค๊ฐœ๋ณ€์ˆ˜",
Expand Down Expand Up @@ -947,7 +950,9 @@
"tencent_cloud": "ํ…์„ผํŠธ ํด๋ผ์šฐ๋“œ",
"volcano_engine": "๋ณผ์ผ€์ด๋…ธ ์—”์ง„",
"minimax": "MiniMax",
"generic_openai": "๋ฒ”์šฉ OpenAI"
"generic_openai": "๋ฒ”์šฉ OpenAI",
"aws_bedrock": "Amazon Bedrock",
"aws_bedrock_mantle": "Amazon Bedrock (OpenAI ํ˜ธํ™˜)"
},
"modelType": {
"llm": "๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ"
Expand Down
Loading