changes for azure foundry support

This commit is contained in:
vikneshbala2021 2026-06-18 16:54:12 +08:00
parent 96096ea0ff
commit 4569b73547
7 changed files with 254 additions and 42 deletions

View File

@ -527,16 +527,39 @@ class OasisProfileGenerator:
for attempt in range(max_attempts):
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=[
kwargs = {
"model": self.model_name,
"messages": [
{"role": "system", "content": self._get_system_prompt(is_individual)},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # 每次重试降低温度
# 不设置max_tokens让LLM自由发挥
)
"response_format": {"type": "json_object"},
# 部分模型不支持自定义温度,后续会自动降级重试
"temperature": 0.7 - (attempt * 0.1)
}
response = None
for _ in range(3):
try:
response = self.client.chat.completions.create(**kwargs)
break
except Exception as e:
err_text = str(e).lower()
if "unsupported parameter" in err_text and "max_tokens" in err_text and "max_tokens" in kwargs:
kwargs.pop("max_tokens", None)
continue
if "temperature" in err_text and (
"unsupported value" in err_text or "default (1) value" in err_text
) and "temperature" in kwargs:
kwargs.pop("temperature", None)
continue
raise
if response is None:
raise RuntimeError("OpenAI chat completion failed after compatibility retries")
content = response.choices[0].message.content

View File

@ -440,16 +440,39 @@ class SimulationConfigGenerator:
for attempt in range(max_attempts):
try:
response = self.client.chat.completions.create(
model=self.model_name,
messages=[
kwargs = {
"model": self.model_name,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # 每次重试降低温度
# 不设置max_tokens让LLM自由发挥
)
"response_format": {"type": "json_object"},
# 部分模型不支持自定义温度,后续会自动降级重试
"temperature": 0.7 - (attempt * 0.1)
}
response = None
for _ in range(3):
try:
response = self.client.chat.completions.create(**kwargs)
break
except Exception as e:
err_text = str(e).lower()
if "unsupported parameter" in err_text and "max_tokens" in err_text and "max_tokens" in kwargs:
kwargs.pop("max_tokens", None)
continue
if "temperature" in err_text and (
"unsupported value" in err_text or "default (1) value" in err_text
) and "temperature" in kwargs:
kwargs.pop("temperature", None)
continue
raise
if response is None:
raise RuntimeError("OpenAI chat completion failed after compatibility retries")
content = response.choices[0].message.content
finish_reason = response.choices[0].finish_reason

View File

@ -1,18 +1,47 @@
"""
LLM客户端封装
统一使用OpenAI格式调用
支持OpenAI和Anthropic格式的API调用
"""
import json
import re
from urllib.parse import urlparse
from typing import Optional, Dict, Any, List
from openai import OpenAI
from ..config import Config
# 尝试导入Anthropic SDK如果不可用则设为None
try:
from anthropic import Anthropic
except ImportError:
Anthropic = None
try:
from anthropic import AnthropicFoundry
except ImportError:
AnthropicFoundry = None
def _foundry_resource_from_endpoint(endpoint: str) -> str:
"""从 Foundry endpoint 提取 resource 名称。"""
if not endpoint:
raise ValueError("LLM_BASE_URL is empty")
if "://" not in endpoint:
return endpoint.strip()
host = urlparse(endpoint).netloc
m = re.match(r"([^.]+)\.services\.ai\.azure\.com$", host)
if not m:
raise ValueError(
f"Could not parse Foundry resource from endpoint: {endpoint!r}. "
"Expected https://<resource>.services.ai.azure.com/anthropic/"
)
return m.group(1)
class LLMClient:
"""LLM客户端"""
"""LLM客户端 - 支持OpenAI和Anthropic格式"""
def __init__(
self,
@ -27,10 +56,60 @@ class LLMClient:
if not self.api_key:
raise ValueError("LLM_API_KEY 未配置")
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
)
# 检测是否是Anthropic格式的端点
self.is_anthropic = "anthropic" in self.base_url.lower()
if self.is_anthropic:
# 使用Anthropic SDK
if Anthropic is None:
raise ImportError("Anthropic SDK未安装。请运行: pip install anthropic")
is_foundry_endpoint = "services.ai.azure.com" in self.base_url.lower()
if is_foundry_endpoint and AnthropicFoundry is not None:
resource = _foundry_resource_from_endpoint(self.base_url)
self.client = AnthropicFoundry(
api_key=self.api_key,
resource=resource,
)
else:
self.client = Anthropic(
api_key=self.api_key,
base_url=self.base_url
)
self.client_type = "anthropic"
else:
# 使用OpenAI SDK
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
)
self.client_type = "openai"
@staticmethod
def _convert_messages_for_anthropic(messages: List[Dict[str, str]]) -> Dict[str, Any]:
"""将 OpenAI 风格消息转换为 Anthropic Messages API 所需格式。"""
system_parts: List[str] = []
anthropic_messages: List[Dict[str, str]] = []
for msg in messages:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "system":
if content:
system_parts.append(content)
continue
if role not in {"user", "assistant"}:
role = "user"
anthropic_messages.append({"role": role, "content": content})
if not anthropic_messages:
anthropic_messages = [{"role": "user", "content": ""}]
result: Dict[str, Any] = {"messages": anthropic_messages}
if system_parts:
result["system"] = "\n\n".join(system_parts)
return result
def chat(
self,
@ -51,18 +130,65 @@ class LLMClient:
Returns:
模型响应文本
"""
kwargs = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
if self.client_type == "anthropic":
# 使用Anthropic SDKsystem 需单独传递messages 仅支持 user/assistant
converted = self._convert_messages_for_anthropic(messages)
create_kwargs = {
"model": self.model,
"messages": converted["messages"],
"system": converted.get("system"),
"max_tokens": max_tokens,
"temperature": temperature,
}
try:
response = self.client.messages.create(**create_kwargs)
except Exception as e:
# 某些新模型会拒绝 temperature 参数(报错: temperature is deprecated
if "temperature" in str(e).lower() and "deprecated" in str(e).lower():
create_kwargs.pop("temperature", None)
response = self.client.messages.create(**create_kwargs)
else:
raise
content = response.content[0].text
else:
# 使用OpenAI SDK
kwargs = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
if response_format:
kwargs["response_format"] = response_format
response = None
for _ in range(3):
try:
response = self.client.chat.completions.create(**kwargs)
break
except Exception as e:
err_text = str(e).lower()
# 某些模型(如部分 GPT-5 系列)不支持 max_tokens仅支持 max_completion_tokens
if "unsupported parameter" in err_text and "max_tokens" in err_text and "max_tokens" in kwargs:
kwargs.pop("max_tokens", None)
kwargs["max_completion_tokens"] = max_tokens
continue
# 某些模型仅支持默认 temperature通常是 1
if "temperature" in err_text and (
"unsupported value" in err_text or "default (1) value" in err_text
) and "temperature" in kwargs:
kwargs.pop("temperature", None)
continue
raise
if response is None:
raise RuntimeError("OpenAI chat completion failed after compatibility retries")
content = response.choices[0].message.content
if response_format:
kwargs["response_format"] = response_format
response = self.client.chat.completions.create(**kwargs)
content = response.choices[0].message.content
# 部分模型如MiniMax M2.5会在content中包含<think>思考内容,需要移除
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
return content
@ -84,14 +210,34 @@ class LLMClient:
Returns:
解析后的JSON对象
"""
response = self.chat(
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
response_format={"type": "json_object"}
)
if self.client_type == "anthropic":
modified_messages = [dict(m) for m in messages]
json_instruction = "IMPORTANT: You must respond with valid JSON only, no other text."
system_index = next((i for i, m in enumerate(modified_messages) if m.get("role") == "system"), None)
if system_index is None:
modified_messages.insert(0, {"role": "system", "content": json_instruction})
else:
modified_messages[system_index]["content"] = (
f"{modified_messages[system_index].get('content', '')}\n\n{json_instruction}"
)
response_text = self.chat(
messages=modified_messages,
temperature=temperature,
max_tokens=max_tokens,
)
else:
# OpenAI格式使用response_format
response = self.chat(
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
response_format={"type": "json_object"}
)
response_text = response
# 清理markdown代码块标记
cleaned_response = response.strip()
cleaned_response = response_text.strip()
cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE)
cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response)
cleaned_response = cleaned_response.strip()

View File

@ -15,6 +15,7 @@ dependencies = [
# LLM 相关
"openai>=1.0.0",
"anthropic>=0.71.0",
# Zep Cloud
"zep-cloud==3.13.0",

View File

@ -12,6 +12,8 @@ flask-cors>=6.0.0
# ============= LLM 相关 =============
# OpenAI SDK统一使用 OpenAI 格式调用 LLM
openai>=1.0.0
# Anthropic SDK支持 Azure Foundry Anthropic 端点)
anthropic>=0.71.0
# ============= Zep Cloud =============
zep-cloud==3.13.0

View File

@ -24,6 +24,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anthropic"
version = "0.109.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "docstring-parser" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/b7/9a8e2f79011e89dd6eeb599c27332aed765dac9d6fbee3a55e68e4e3ec25/anthropic-0.109.2.tar.gz", hash = "sha256:d37db299597c7bc124b49b767ff135f1e6456b64af2b2fad4b63b2a1df333cf0", size = 927559, upload-time = "2026-06-15T17:30:25.024Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/f2/bee5de8a2699fc8a3cce34d61c7a2626a2c310ddde7ea5611327eb0ddbe9/anthropic-0.109.2-py3-none-any.whl", hash = "sha256:e0fb4ca5df0ed983248c9c6c3242adc81d9cfddb8725902da53698554117abac", size = 923800, upload-time = "2026-06-15T17:30:23.124Z" },
]
[[package]]
name = "anyio"
version = "4.12.0"
@ -988,6 +1007,7 @@ name = "mirofish-backend"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },
{ name = "camel-ai" },
{ name = "camel-oasis" },
{ name = "chardet" },
@ -1016,6 +1036,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "anthropic", specifier = ">=0.71.0" },
{ name = "camel-ai", specifier = "==0.2.78" },
{ name = "camel-oasis", specifier = "==0.2.5" },
{ name = "chardet", specifier = ">=5.0.0" },

View File

@ -1435,7 +1435,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@ -1913,7 +1912,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -2053,7 +2051,6 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -2128,7 +2125,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",