mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-22 19:47:28 +00:00
fix: cloud chat /chat startup failures (422 + Azure api_version) (#1605)
This commit is contained in:
parent
819344fc2f
commit
caf4affdfe
6 changed files with 110 additions and 32 deletions
|
|
@ -24,7 +24,10 @@ from camel.types import ModelPlatformType
|
|||
|
||||
from app.agent.listen_chat_agent import ListenChatAgent, logger
|
||||
from app.model.chat import AgentModelConfig, Chat
|
||||
from app.model.model_platform import patch_bedrock_cloud_config
|
||||
from app.model.model_platform import (
|
||||
patch_azure_cloud_config,
|
||||
patch_bedrock_cloud_config,
|
||||
)
|
||||
from app.service.task import ActionCreateAgentData, Agents, get_task_lock
|
||||
from app.utils.event_loop_utils import _schedule_async_task
|
||||
|
||||
|
|
@ -89,6 +92,13 @@ def agent_model(
|
|||
effective_config["api_url"], extra_params = patch_bedrock_cloud_config(
|
||||
effective_config["api_url"], extra_params
|
||||
)
|
||||
# Cloud mode: default api_version for Azure-backed models so AzureOpenAI
|
||||
# construction does not blow up when the frontend omits extra_params.
|
||||
if (
|
||||
effective_config.get("model_platform") == "azure"
|
||||
and options.is_cloud()
|
||||
):
|
||||
extra_params = patch_azure_cloud_config(extra_params)
|
||||
init_param_keys = {
|
||||
"api_version",
|
||||
"azure_ad_token",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ from app.agent.prompt import MCP_SYS_PROMPT
|
|||
from app.agent.toolkit.mcp_search_toolkit import McpSearchToolkit
|
||||
from app.agent.tools import get_mcp_tools
|
||||
from app.model.chat import Chat
|
||||
from app.model.model_platform import patch_bedrock_cloud_config
|
||||
from app.model.model_platform import (
|
||||
patch_azure_cloud_config,
|
||||
patch_bedrock_cloud_config,
|
||||
)
|
||||
from app.service.task import ActionCreateAgentData, Agents, get_task_lock
|
||||
|
||||
|
||||
|
|
@ -86,6 +89,8 @@ async def mcp_agent(options: Chat):
|
|||
api_url, extra_params = patch_bedrock_cloud_config(
|
||||
api_url, extra_params
|
||||
)
|
||||
if options.model_platform == "azure" and options.is_cloud():
|
||||
extra_params = patch_azure_cloud_config(extra_params)
|
||||
|
||||
# Build model_config_dict with prompt caching
|
||||
model_config_dict = {}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ PLATFORM_ALIAS_MAPPING: Final[dict[str, str]] = {
|
|||
# Bedrock Converse requires a region during model initialization.
|
||||
BEDROCK_CONVERSE_REGION: Final[str] = "us-west-2"
|
||||
|
||||
# Azure OpenAI requires an api_version. The cloud proxy accepts any modern
|
||||
# version; this default keeps cloud-mode requests working when the frontend
|
||||
# does not surface api_version in extra_params.
|
||||
AZURE_DEFAULT_API_VERSION: Final[str] = "2024-10-21"
|
||||
|
||||
|
||||
def patch_bedrock_cloud_config(
|
||||
api_url: str, extra_params: dict
|
||||
|
|
@ -43,6 +48,20 @@ def patch_bedrock_cloud_config(
|
|||
return api_url, extra_params
|
||||
|
||||
|
||||
def patch_azure_cloud_config(extra_params: dict) -> dict:
|
||||
"""Default Azure `api_version` for cloud mode.
|
||||
|
||||
The cloud proxy fronts Azure OpenAI but the frontend sends an empty
|
||||
`extra_params` for cloud, leaving `api_version` unset. Camel's
|
||||
`AzureOpenAIModel` raises if neither the kwarg nor `AZURE_API_VERSION`
|
||||
env var is provided — inject a sensible default here so cloud-mode
|
||||
GPT models (gpt-5.4, gpt-5.5, gpt-5-mini, ...) construct cleanly.
|
||||
"""
|
||||
extra_params = dict(extra_params)
|
||||
extra_params.setdefault("api_version", AZURE_DEFAULT_API_VERSION)
|
||||
return extra_params
|
||||
|
||||
|
||||
def normalize_model_platform(platform: str) -> str:
|
||||
"""Normalize provider aliases to supported model platform names."""
|
||||
return PLATFORM_ALIAS_MAPPING.get(platform, platform)
|
||||
|
|
|
|||
|
|
@ -72,18 +72,16 @@ const cloudModelOptions = [
|
|||
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro Preview' },
|
||||
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview' },
|
||||
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview' },
|
||||
{ id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini' },
|
||||
{ id: 'gpt-4.1', name: 'GPT-4.1' },
|
||||
{ id: 'gpt-5', name: 'GPT-5' },
|
||||
{ id: 'gpt-5.1', name: 'GPT-5.1' },
|
||||
{ id: 'gpt-5.2', name: 'GPT-5.2' },
|
||||
{ id: 'gpt-5.4', name: 'GPT-5.4' },
|
||||
{ id: 'gpt-5.5', name: 'GPT-5.5' },
|
||||
{ id: 'gpt-5-mini', name: 'GPT-5 Mini' },
|
||||
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },
|
||||
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
||||
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
|
||||
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6' },
|
||||
{ id: 'minimax_m2_5', name: 'Minimax M2.5' },
|
||||
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7' },
|
||||
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' },
|
||||
{ id: 'minimax_m2_7', name: 'Minimax M2.7' },
|
||||
] as const;
|
||||
|
||||
export interface ChatInputModelDropdownProps {
|
||||
|
|
@ -381,9 +379,9 @@ export function ChatInputModelDropdown({
|
|||
}
|
||||
)}
|
||||
>
|
||||
<span className="gap-1.5 min-w-0 inline-flex min-h-[1.25rem] items-center overflow-hidden">
|
||||
<span className="inline-flex min-h-[1.25rem] min-w-0 items-center gap-1.5 overflow-hidden">
|
||||
<Sparkles className="size-3.5 shrink-0" strokeWidth={2} aria-hidden />
|
||||
<span className="!text-label-xs min-w-0 font-semibold truncate">
|
||||
<span className="min-w-0 truncate !text-label-xs font-semibold">
|
||||
{triggerModelName}
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -407,19 +405,19 @@ export function ChatInputModelDropdown({
|
|||
className={cn(
|
||||
modelTriggerShellClass,
|
||||
'min-w-0 cursor-pointer border-0 text-left',
|
||||
'font-semibold justify-between transition-colors',
|
||||
'justify-between font-semibold transition-colors',
|
||||
'hover:bg-ds-bg-neutral-subtle-hover active:bg-ds-bg-neutral-subtle-default',
|
||||
'focus-visible:ring-ds-border-neutral-strong-default focus-visible:ring-offset-ds-bg-neutral-default-default focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ds-border-neutral-strong-default focus-visible:ring-offset-2 focus-visible:ring-offset-ds-bg-neutral-default-default',
|
||||
'disabled:pointer-events-none disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<span className="gap-1.5 min-w-0 flex flex-1 items-center overflow-hidden">
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
|
||||
<Sparkles
|
||||
className="size-3.5 shrink-0"
|
||||
strokeWidth={2}
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="!text-label-xs min-w-0 text-ds-text-neutral-default-default flex-1 truncate text-left">
|
||||
<span className="min-w-0 flex-1 truncate text-left !text-label-xs text-ds-text-neutral-default-default">
|
||||
{triggerModelName}
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -446,7 +444,7 @@ export function ChatInputModelDropdown({
|
|||
}}
|
||||
>
|
||||
<DropdownMenuSubTrigger
|
||||
className="gap-2 min-w-0 [&>svg:first-child]:!h-4 [&>svg:first-child]:!w-4 [&>svg:first-child]:!min-h-4 [&>svg:first-child]:!min-w-4 flex w-full items-center justify-start"
|
||||
className="flex w-full min-w-0 items-center justify-start gap-2 [&>svg:first-child]:!h-4 [&>svg:first-child]:!min-h-4 [&>svg:first-child]:!w-4 [&>svg:first-child]:!min-w-4"
|
||||
onPointerEnter={(e) => {
|
||||
activeSubTriggerRef.current = e.currentTarget;
|
||||
}}
|
||||
|
|
@ -454,10 +452,10 @@ export function ChatInputModelDropdown({
|
|||
<img
|
||||
src={folderIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 mt-0.5 shrink-0"
|
||||
className="mt-0.5 h-4 w-4 shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="text-body-sm min-w-0 flex-1 text-left">
|
||||
<span className="min-w-0 flex-1 text-left text-body-sm">
|
||||
{t('setting.eigent-cloud')}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
|
@ -492,16 +490,16 @@ export function ChatInputModelDropdown({
|
|||
}}
|
||||
>
|
||||
<DropdownMenuSubTrigger
|
||||
className="gap-2 min-w-0 [&>svg:first-child]:!h-5 [&>svg:first-child]:!w-4 [&>svg:first-child]:!min-h-4 [&>svg:first-child]:!min-w-4 flex w-full items-center justify-start"
|
||||
className="flex w-full min-w-0 items-center justify-start gap-2 [&>svg:first-child]:!h-5 [&>svg:first-child]:!min-h-4 [&>svg:first-child]:!w-4 [&>svg:first-child]:!min-w-4"
|
||||
onPointerEnter={(e) => {
|
||||
activeSubTriggerRef.current = e.currentTarget;
|
||||
}}
|
||||
>
|
||||
<Layers
|
||||
className="text-ds-icon-neutral-default-default shrink-0"
|
||||
className="shrink-0 text-ds-icon-neutral-default-default"
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="text-body-sm min-w-0 flex-1 text-left">
|
||||
<span className="min-w-0 flex-1 text-left text-body-sm">
|
||||
{t('setting.custom-model')}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
|
@ -525,7 +523,7 @@ export function ChatInputModelDropdown({
|
|||
}}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -546,15 +544,15 @@ export function ChatInputModelDropdown({
|
|||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="gap-1 flex items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
{!isConfigured && (
|
||||
<div className="h-2 w-2 bg-ds-text-neutral-subtle-default rounded-full opacity-10" />
|
||||
<div className="h-2 w-2 rounded-full bg-ds-text-neutral-subtle-default opacity-10" />
|
||||
)}
|
||||
{isPreferred && (
|
||||
<Check className="h-4 w-4 text-ds-text-success-default-default" />
|
||||
)}
|
||||
{isConfigured && !isPreferred && (
|
||||
<div className="h-2 w-2 bg-ds-text-success-default-default rounded-full" />
|
||||
<div className="h-2 w-2 rounded-full bg-ds-text-success-default-default" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -569,16 +567,16 @@ export function ChatInputModelDropdown({
|
|||
}}
|
||||
>
|
||||
<DropdownMenuSubTrigger
|
||||
className="gap-2 min-w-0 [&>svg:first-child]:!h-4 [&>svg:first-child]:!w-4 [&>svg:first-child]:!min-h-4 [&>svg:first-child]:!min-w-4 flex w-full items-center justify-start"
|
||||
className="flex w-full min-w-0 items-center justify-start gap-2 [&>svg:first-child]:!h-4 [&>svg:first-child]:!min-h-4 [&>svg:first-child]:!w-4 [&>svg:first-child]:!min-w-4"
|
||||
onPointerEnter={(e) => {
|
||||
activeSubTriggerRef.current = e.currentTarget;
|
||||
}}
|
||||
>
|
||||
<HardDrive
|
||||
className="text-ds-icon-neutral-default-default shrink-0"
|
||||
className="shrink-0 text-ds-icon-neutral-default-default"
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="text-body-sm min-w-0 flex-1 text-left">
|
||||
<span className="min-w-0 flex-1 text-left text-body-sm">
|
||||
{t('setting.local-model')}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
|
@ -602,7 +600,7 @@ export function ChatInputModelDropdown({
|
|||
}}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -623,15 +621,15 @@ export function ChatInputModelDropdown({
|
|||
{model.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="gap-1 flex items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
{!isConfigured && (
|
||||
<div className="h-2 w-2 bg-ds-text-neutral-subtle-default rounded-full opacity-10" />
|
||||
<div className="h-2 w-2 rounded-full bg-ds-text-neutral-subtle-default opacity-10" />
|
||||
)}
|
||||
{isPreferred && (
|
||||
<Check className="h-4 w-4 text-ds-text-success-default-default" />
|
||||
)}
|
||||
{isConfigured && !isPreferred && (
|
||||
<div className="h-2 w-2 bg-ds-text-success-default-default rounded-full" />
|
||||
<div className="h-2 w-2 rounded-full bg-ds-text-success-default-default" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -136,6 +136,27 @@ const getRandomDefaultModel = (): CloudModelType => {
|
|||
return models[Math.floor(Math.random() * models.length)];
|
||||
};
|
||||
|
||||
const SUPPORTED_CLOUD_MODEL_TYPES: ReadonlySet<CloudModelType> =
|
||||
new Set<CloudModelType>([
|
||||
'gemini-3.1-pro-preview',
|
||||
'gemini-3-pro-preview',
|
||||
'gemini-3-flash-preview',
|
||||
'claude-haiku-4-5',
|
||||
'claude-sonnet-4-5',
|
||||
'claude-sonnet-4-6',
|
||||
'claude-opus-4-6',
|
||||
'claude-opus-4-7',
|
||||
'gpt-5.4',
|
||||
'gpt-5.5',
|
||||
'gpt-5-mini',
|
||||
'deepseek-v4-pro',
|
||||
'minimax_m2_7',
|
||||
]);
|
||||
|
||||
const isSupportedCloudModelType = (value: unknown): value is CloudModelType =>
|
||||
typeof value === 'string' &&
|
||||
SUPPORTED_CLOUD_MODEL_TYPES.has(value as CloudModelType);
|
||||
|
||||
// create store
|
||||
const authStore = create<AuthState>()(
|
||||
persist(
|
||||
|
|
@ -306,7 +327,7 @@ const authStore = create<AuthState>()(
|
|||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
version: 6,
|
||||
version: 7,
|
||||
migrate: (persistedState, _version) => {
|
||||
const s = persistedState as
|
||||
| {
|
||||
|
|
@ -314,10 +335,19 @@ const authStore = create<AuthState>()(
|
|||
appearanceMode?: AppearanceMode;
|
||||
customThemeCatalog?: Partial<ThemeCatalog>;
|
||||
workspaceMainBackground?: string;
|
||||
cloud_model_type?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
if (!s) return persistedState as typeof persistedState;
|
||||
|
||||
// Drop unsupported cloud model ids so stale values like 'gpt-5.2'
|
||||
// (removed from the chat input dropdown) don't keep submitting an
|
||||
// empty model_platform and triggering 422 on /chat.
|
||||
const sanitizedCloudModelType: CloudModelType =
|
||||
isSupportedCloudModelType(s.cloud_model_type)
|
||||
? s.cloud_model_type
|
||||
: getRandomDefaultModel();
|
||||
|
||||
const rawWmb = s.workspaceMainBackground;
|
||||
let workspaceMainBackground: WorkspaceMainBackground = 'empty';
|
||||
if (
|
||||
|
|
@ -352,6 +382,7 @@ const authStore = create<AuthState>()(
|
|||
appearanceMode: 'light',
|
||||
customThemeCatalog: normalizedCustomCatalog,
|
||||
workspaceMainBackground,
|
||||
cloud_model_type: sanitizedCloudModelType,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
|
@ -360,6 +391,7 @@ const authStore = create<AuthState>()(
|
|||
appearanceMode: normalizedAppearanceMode,
|
||||
customThemeCatalog: normalizedCustomCatalog,
|
||||
workspaceMainBackground,
|
||||
cloud_model_type: sanitizedCloudModelType,
|
||||
} as typeof persistedState;
|
||||
},
|
||||
partialize: (state) => ({
|
||||
|
|
|
|||
|
|
@ -15,9 +15,20 @@
|
|||
// Usage: npm run dev:web | npm run build:web
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import path from 'node:path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
const proxyHttpsAgent = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30_000,
|
||||
});
|
||||
const proxyHttpAgent = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30_000,
|
||||
});
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
return {
|
||||
|
|
@ -44,6 +55,9 @@ export default defineConfig(({ mode }) => {
|
|||
'/api': {
|
||||
target: env.VITE_PROXY_URL,
|
||||
changeOrigin: true,
|
||||
agent: env.VITE_PROXY_URL.startsWith('https')
|
||||
? proxyHttpsAgent
|
||||
: proxyHttpAgent,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue