mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-19 05:45:26 +00:00
🐛 fix(bedrock): add parameter conflict handling for Claude 4+ models (#9627)
* fix(bedrock): add parameter conflict handling for Claude 4+ models - Add logic to prevent sending both temperature and top_p for Claude 4+ models - Matches existing implementation in Anthropic provider - Fixes ValidationException error for Claude 4.5 models via Bedrock - Includes support for both standard and Bedrock-specific model IDs Fixes #9523 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com> * 🧪 test(bedrock): add parameter conflict handling tests Add comprehensive tests for Claude 4+ models parameter conflict detection: - Test temperature preference over top_p when both provided - Test top_p usage when temperature not provided - Test both parameters allowed for non-Claude-4+ models - Test standard and Bedrock-specific model ID formats - Test US region model IDs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com> * 🔧 fix: make temperature optional in ChatStreamPayload interface The temperature property should be optional to support test cases and scenarios where only top_p is provided. This resolves TypeScript error TS2741 in Bedrock provider tests. Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com> * 🔧 fix: correct test assertions to handle JSON.stringify undefined omission - Remove undefined properties from test expectations since JSON.stringify omits them - Fix temperature/top_p conflict test assertions for Claude 4+ models - Ensure tests match actual JSON serialization behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com> * 🔧 fix: add null safety checks for optional temperature parameter - Added proper undefined checks before temperature arithmetic operations in anthropic and bedrock providers - Added null checks before temperature comparisons in groq, perplexity, and search1api providers - Resolves TS18048 errors where temperature is possibly undefined - Maintains existing logic while satisfying TypeScript strict null checks Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com> * refactor with parameterResolver * upgrade * upgrade swr * refactor context-builder --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
This commit is contained in:
+60
-60
@@ -129,20 +129,20 @@
|
||||
"@azure-rest/ai-inference": "1.0.0-beta.5",
|
||||
"@azure/core-auth": "^1.10.1",
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"@clerk/localizations": "^3.25.0",
|
||||
"@clerk/nextjs": "^6.31.10",
|
||||
"@clerk/themes": "^2.4.18",
|
||||
"@clerk/localizations": "^3.25.7",
|
||||
"@clerk/nextjs": "^6.33.3",
|
||||
"@clerk/themes": "^2.4.25",
|
||||
"@codesandbox/sandpack-react": "^2.20.0",
|
||||
"@cyntler/react-doc-viewer": "^1.17.0",
|
||||
"@cyntler/react-doc-viewer": "^1.17.1",
|
||||
"@electric-sql/pglite": "0.2.17",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@fal-ai/client": "^1.6.2",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@google/genai": "^1.19.0",
|
||||
"@google/genai": "^1.24.0",
|
||||
"@huggingface/inference": "^2.8.1",
|
||||
"@icons-pack/react-simple-icons": "^13.8.0",
|
||||
"@khmyznikov/pwa-install": "0.3.9",
|
||||
"@langchain/community": "^0.3.55",
|
||||
"@langchain/community": "^0.3.57",
|
||||
"@lobechat/agent-runtime": "workspace:*",
|
||||
"@lobechat/const": "workspace:*",
|
||||
"@lobechat/context-engine": "workspace:*",
|
||||
@@ -160,48 +160,48 @@
|
||||
"@lobehub/charts": "^2.1.2",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
||||
"@lobehub/editor": "^1.11.0",
|
||||
"@lobehub/icons": "^2.32.2",
|
||||
"@lobehub/editor": "^1.16.0",
|
||||
"@lobehub/icons": "^2.42.0",
|
||||
"@lobehub/market-sdk": "^0.22.7",
|
||||
"@lobehub/tts": "^2.0.1",
|
||||
"@lobehub/ui": "^2.13.0",
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@next/third-parties": "^15.5.3",
|
||||
"@lobehub/ui": "^2.13.2",
|
||||
"@modelcontextprotocol/sdk": "^1.20.0",
|
||||
"@neondatabase/serverless": "^1.0.2",
|
||||
"@next/third-parties": "^15.5.4",
|
||||
"@react-spring/web": "^9.7.5",
|
||||
"@serwist/next": "^9.2.1",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/next": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/next": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/edge-config": "^1.4.0",
|
||||
"@vercel/functions": "^3.0.0",
|
||||
"@vercel/functions": "^3.1.3",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ahooks": "^3.9.5",
|
||||
"antd": "^5.27.3",
|
||||
"antd": "^5.27.4",
|
||||
"antd-style": "^3.7.1",
|
||||
"brotli-wasm": "^3.0.1",
|
||||
"chroma-js": "^3.1.2",
|
||||
"cookie": "^1.0.2",
|
||||
"countries-and-timezones": "^3.8.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"debug": "^4.4.1",
|
||||
"debug": "^4.4.3",
|
||||
"dexie": "^3.2.7",
|
||||
"diff": "^8.0.2",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"epub2": "^3.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-type": "^21.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"gpt-tokenizer": "^3.0.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"gpt-tokenizer": "^3.2.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-text": "^9.0.5",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"idb-keyval": "^6.2.2",
|
||||
@@ -210,24 +210,24 @@
|
||||
"js-sha256": "^0.11.1",
|
||||
"jsonl-parse-stringify": "^1.0.3",
|
||||
"keyv": "^4.5.4",
|
||||
"langchain": "^0.3.33",
|
||||
"langchain": "^0.3.35",
|
||||
"langfuse": "^3.38.5",
|
||||
"langfuse-core": "^3.38.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mammoth": "^1.10.0",
|
||||
"mammoth": "^1.11.0",
|
||||
"markdown-to-txt": "^2.0.1",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"model-bank": "workspace:*",
|
||||
"modern-screenshot": "^4.6.6",
|
||||
"nanoid": "^5.1.5",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "~15.3.5",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"numeral": "^2.0.6",
|
||||
"nuqs": "^2.6.0",
|
||||
"nuqs": "^2.7.1",
|
||||
"officeparser": "5.1.1",
|
||||
"oidc-provider": "^9.5.1",
|
||||
"ollama": "^0.6.0",
|
||||
@@ -238,43 +238,43 @@
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdfjs-dist": "4.8.69",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^9.9.5",
|
||||
"pino": "^9.13.1",
|
||||
"plaiceholder": "^3.0.0",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-js": "^1.264.2",
|
||||
"posthog-js": "^1.275.1",
|
||||
"pure-rand": "^7.0.1",
|
||||
"pwa-install-handler": "^2.6.3",
|
||||
"query-string": "^9.3.0",
|
||||
"query-string": "^9.3.1",
|
||||
"random-words": "^2.0.1",
|
||||
"react": "^19.1.1",
|
||||
"react": "^19.2.0",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hotkeys-hook": "^5.1.0",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-layout-kit": "^2.0.0",
|
||||
"react-lazy-load": "^4.0.1",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-scan": "^0.4.3",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-html": "^16.0.1",
|
||||
"resolve-accept-language": "^3.1.13",
|
||||
"rtl-detect": "^1.1.2",
|
||||
"semver": "^7.7.2",
|
||||
"sharp": "^0.34.3",
|
||||
"shiki": "^3.12.2",
|
||||
"semver": "^7.7.3",
|
||||
"sharp": "^0.34.4",
|
||||
"shiki": "^3.13.0",
|
||||
"ssrf-safe-fetch": "workspace:*",
|
||||
"stripe": "^17.7.0",
|
||||
"superjson": "^2.2.2",
|
||||
"svix": "^1.76.1",
|
||||
"swr": "2.3.4",
|
||||
"svix": "^1.77.0",
|
||||
"swr": "^2.3.6",
|
||||
"systemjs": "^6.15.1",
|
||||
"tokenx": "^1.0.0",
|
||||
"ts-md5": "^2.0.0",
|
||||
"tokenx": "^1.1.0",
|
||||
"ts-md5": "^2.0.1",
|
||||
"ua-parser-js": "^1.0.41",
|
||||
"unstructured-client": "^0.19.0",
|
||||
"url-join": "^5.0.0",
|
||||
@@ -290,14 +290,14 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@edge-runtime/vm": "^5.0.0",
|
||||
"@huggingface/tasks": "^0.19.0",
|
||||
"@huggingface/tasks": "^0.19.50",
|
||||
"@lobechat/types": "workspace:*",
|
||||
"@lobehub/i18n-cli": "^1.25.1",
|
||||
"@lobehub/lint": "^1.26.2",
|
||||
"@lobehub/market-types": "^1.11.4",
|
||||
"@lobehub/seo-cli": "^1.7.0",
|
||||
"@next/bundle-analyzer": "^15.5.3",
|
||||
"@next/eslint-plugin-next": "^15.5.3",
|
||||
"@next/bundle-analyzer": "^15.5.4",
|
||||
"@next/eslint-plugin-next": "^15.5.4",
|
||||
"@peculiar/webcrypto": "^1.5.0",
|
||||
"@prettier/sync": "^0.6.1",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
@@ -312,12 +312,12 @@
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.18.1",
|
||||
"@types/node": "^22.18.9",
|
||||
"@types/numeral": "^2.0.5",
|
||||
"@types/oidc-provider": "^9.5.0",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@types/rtl-detect": "^1.0.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/systemjs": "^6.15.3",
|
||||
@@ -329,28 +329,28 @@
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"commitlint": "^19.8.1",
|
||||
"consola": "^3.4.2",
|
||||
"cross-env": "^10.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dbdocs": "^0.16.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"dbdocs": "^0.16.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv-expand": "^12.0.3",
|
||||
"dpdm-fast": "^1.0.13",
|
||||
"dpdm-fast": "^1.0.14",
|
||||
"drizzle-dbml-generator": "^0.10.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-mdx": "^3.6.2",
|
||||
"fake-indexeddb": "^6.2.2",
|
||||
"fs-extra": "^11.3.1",
|
||||
"fake-indexeddb": "^6.2.3",
|
||||
"fs-extra": "^11.3.2",
|
||||
"glob": "^11.0.3",
|
||||
"happy-dom": "^18.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"import-in-the-middle": "^1.14.2",
|
||||
"import-in-the-middle": "^1.15.0",
|
||||
"just-diff": "^6.0.2",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-table": "^3.0.4",
|
||||
"mcp-hello-world": "^1.1.2",
|
||||
"mime": "^4.0.7",
|
||||
"mime": "^4.1.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-gyp": "^11.4.2",
|
||||
"openapi-typescript": "^7.9.1",
|
||||
@@ -364,12 +364,12 @@
|
||||
"semantic-release": "^21.1.2",
|
||||
"serwist": "^9.2.1",
|
||||
"stylelint": "^15.11.0",
|
||||
"tsx": "^4.20.5",
|
||||
"tsx": "^4.20.6",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^7.1.5",
|
||||
"vite": "^7.1.9",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.0",
|
||||
|
||||
+6
-6
@@ -1,25 +1,25 @@
|
||||
import { OpenAI } from 'openai';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { OpenAIChatMessage, UserMessageContentPart } from '../types/chat';
|
||||
import { imageUrlToBase64 } from '../utils/imageToBase64';
|
||||
import { OpenAIChatMessage, UserMessageContentPart } from '../../types/chat';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import {
|
||||
buildAnthropicBlock,
|
||||
buildAnthropicMessage,
|
||||
buildAnthropicMessages,
|
||||
buildAnthropicTools,
|
||||
} from './anthropicHelpers';
|
||||
import { parseDataUri } from './uriParser';
|
||||
} from './anthropic';
|
||||
|
||||
// Mock the parseDataUri function since it's an implementation detail
|
||||
vi.mock('./uriParser', () => ({
|
||||
vi.mock('../../utils/uriParser', () => ({
|
||||
parseDataUri: vi.fn().mockReturnValue({
|
||||
mimeType: 'image/jpeg',
|
||||
base64: 'base64EncodedString',
|
||||
type: 'base64',
|
||||
}),
|
||||
}));
|
||||
vi.mock('../utils/imageToBase64');
|
||||
vi.mock('../../utils/imageToBase64');
|
||||
|
||||
describe('anthropicHelpers', () => {
|
||||
describe('buildAnthropicBlock', () => {
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import { OpenAIChatMessage, UserMessageContentPart } from '../types';
|
||||
import { imageUrlToBase64 } from '../utils/imageToBase64';
|
||||
import { parseDataUri } from './uriParser';
|
||||
import { OpenAIChatMessage, UserMessageContentPart } from '../../types';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
export const buildAnthropicBlock = async (
|
||||
content: UserMessageContentPart,
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
import OpenAI from 'openai';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { imageUrlToBase64 } from './imageToBase64';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import {
|
||||
convertImageUrlToFile,
|
||||
convertMessageContent,
|
||||
convertOpenAIMessages,
|
||||
convertOpenAIResponseInputs,
|
||||
} from './openaiHelpers';
|
||||
import { parseDataUri } from './uriParser';
|
||||
} from './openai';
|
||||
|
||||
// 模拟依赖
|
||||
vi.mock('./imageToBase64');
|
||||
vi.mock('./uriParser');
|
||||
vi.mock('../../utils/imageToBase64');
|
||||
vi.mock('../../utils/uriParser');
|
||||
|
||||
describe('convertMessageContent', () => {
|
||||
beforeEach(() => {
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import OpenAI, { toFile } from 'openai';
|
||||
|
||||
import { disableStreamModels, systemToUserModels } from '../const/models';
|
||||
import { ChatStreamPayload, OpenAIChatMessage } from '../types';
|
||||
import { imageUrlToBase64 } from './imageToBase64';
|
||||
import { parseDataUri } from './uriParser';
|
||||
import { disableStreamModels, systemToUserModels } from '../../const/models';
|
||||
import { ChatStreamPayload, OpenAIChatMessage } from '../../types';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
export const convertMessageContent = async (
|
||||
content: OpenAI.ChatCompletionContentPart,
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { convertSenseNovaMessage } from './sensenovaHelpers';
|
||||
import { convertSenseNovaMessage } from './sensenova';
|
||||
|
||||
describe('convertSenseNovaMessage', () => {
|
||||
it('should convert string content to text type array', () => {
|
||||
@@ -6,8 +6,8 @@ import OpenAI from 'openai';
|
||||
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import { getModelPricing } from '../../utils/getModelPricing';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { convertImageUrlToFile } from '../../utils/openaiHelpers';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import { convertImageUrlToFile } from '../contextBuilders/openai';
|
||||
import { convertOpenAIImageUsage } from '../usageConverters/openai';
|
||||
|
||||
const log = createDebug('lobe-image:openai-compatible');
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Stream } from 'openai/streaming';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import * as openaiHelpers from '../../utils/openaiHelpers';
|
||||
import * as openaiHelpers from '../contextBuilders/openai';
|
||||
import { createOpenAICompatibleRuntime } from './index';
|
||||
|
||||
const sleep = async (ms: number) =>
|
||||
|
||||
@@ -29,10 +29,10 @@ import { desensitizeUrl } from '../../utils/desensitizeUrl';
|
||||
import { getModelPropertyWithFallback } from '../../utils/getFallbackModelProperty';
|
||||
import { getModelPricing } from '../../utils/getModelPricing';
|
||||
import { handleOpenAIError } from '../../utils/handleOpenAIError';
|
||||
import { convertOpenAIMessages, convertOpenAIResponseInputs } from '../../utils/openaiHelpers';
|
||||
import { postProcessModelList } from '../../utils/postProcessModelList';
|
||||
import { StreamingResponse } from '../../utils/response';
|
||||
import { LobeRuntimeAI } from '../BaseAI';
|
||||
import { convertOpenAIMessages, convertOpenAIResponseInputs } from '../contextBuilders/openai';
|
||||
import { OpenAIResponsesStream, OpenAIStream, OpenAIStreamOptions } from '../streams';
|
||||
import { createOpenAICompatibleImage } from './createImage';
|
||||
import { transformResponseAPIToStream, transformResponseToStream } from './nonStreamToStream';
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
MODEL_PARAMETER_CONFLICTS,
|
||||
createParameterResolver,
|
||||
resolveParameters,
|
||||
} from './parameterResolver';
|
||||
|
||||
describe('resolveParameters', () => {
|
||||
describe('Basic functionality', () => {
|
||||
it('should return empty object when no parameters are provided', () => {
|
||||
const result = resolveParameters({}, {});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should normalize temperature by dividing by 2 by default', () => {
|
||||
const result = resolveParameters({ temperature: 1 }, {});
|
||||
expect(result).toEqual({ temperature: 0.5 });
|
||||
});
|
||||
|
||||
it('should not normalize temperature when normalizeTemperature is false', () => {
|
||||
const result = resolveParameters({ temperature: 1 }, { normalizeTemperature: false });
|
||||
expect(result).toEqual({ temperature: 1 });
|
||||
});
|
||||
|
||||
it('should pass through top_p unchanged', () => {
|
||||
const result = resolveParameters({ top_p: 0.9 }, {});
|
||||
expect(result).toEqual({ top_p: 0.9 });
|
||||
});
|
||||
|
||||
it('should return both parameters when no conflict', () => {
|
||||
const result = resolveParameters({ temperature: 1, top_p: 0.9 }, { hasConflict: false });
|
||||
expect(result).toEqual({ temperature: 0.5, top_p: 0.9 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conflict handling', () => {
|
||||
it('should prefer temperature over top_p when hasConflict is true', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 1, top_p: 0.9 },
|
||||
{ hasConflict: true, preferTemperature: true },
|
||||
);
|
||||
expect(result).toEqual({ temperature: 0.5 });
|
||||
});
|
||||
|
||||
it('should prefer top_p over temperature when preferTemperature is false', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 1, top_p: 0.9 },
|
||||
{ hasConflict: true, preferTemperature: false },
|
||||
);
|
||||
expect(result).toEqual({ top_p: 0.9 });
|
||||
});
|
||||
|
||||
it('should return temperature when only temperature is provided with conflict', () => {
|
||||
const result = resolveParameters({ temperature: 1 }, { hasConflict: true });
|
||||
expect(result).toEqual({ temperature: 0.5 });
|
||||
});
|
||||
|
||||
it('should return top_p when only top_p is provided with conflict', () => {
|
||||
const result = resolveParameters({ top_p: 0.9 }, { hasConflict: true });
|
||||
expect(result).toEqual({ top_p: 0.9 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Range constraints', () => {
|
||||
it('should apply temperature min constraint', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 0.02 }, // 0.02 / 2 = 0.01
|
||||
{ temperatureRange: { min: 0.05 } },
|
||||
);
|
||||
expect(result).toEqual({ temperature: 0.05 });
|
||||
});
|
||||
|
||||
it('should apply temperature max constraint', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 2 }, // 2 / 2 = 1
|
||||
{ temperatureRange: { max: 0.99 } },
|
||||
);
|
||||
expect(result).toEqual({ temperature: 0.99 });
|
||||
});
|
||||
|
||||
it('should apply top_p min constraint', () => {
|
||||
const result = resolveParameters({ top_p: 0.005 }, { topPRange: { min: 0.01 } });
|
||||
expect(result).toEqual({ top_p: 0.01 });
|
||||
});
|
||||
|
||||
it('should apply top_p max constraint', () => {
|
||||
const result = resolveParameters({ top_p: 1.5 }, { topPRange: { max: 0.99 } });
|
||||
expect(result).toEqual({ top_p: 0.99 });
|
||||
});
|
||||
|
||||
it('should apply both min and max constraints', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 0.02, top_p: 0.005 },
|
||||
{
|
||||
temperatureRange: { max: 0.99, min: 0.01 },
|
||||
topPRange: { max: 0.99, min: 0.01 },
|
||||
},
|
||||
);
|
||||
expect(result).toEqual({ temperature: 0.01, top_p: 0.01 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Additional parameters', () => {
|
||||
it('should handle frequency_penalty', () => {
|
||||
const result = resolveParameters({ frequency_penalty: 0.5 }, {});
|
||||
expect(result).toEqual({ frequency_penalty: 0.5 });
|
||||
});
|
||||
|
||||
it('should handle presence_penalty', () => {
|
||||
const result = resolveParameters({ presence_penalty: 0.5 }, {});
|
||||
expect(result).toEqual({ presence_penalty: 0.5 });
|
||||
});
|
||||
|
||||
it('should handle max_tokens', () => {
|
||||
const result = resolveParameters({ max_tokens: 1000 }, {});
|
||||
expect(result).toEqual({ max_tokens: 1000 });
|
||||
});
|
||||
|
||||
it('should apply frequency_penalty range constraints', () => {
|
||||
const result = resolveParameters(
|
||||
{ frequency_penalty: 3 },
|
||||
{ frequencyPenaltyRange: { max: 2, min: -2 } },
|
||||
);
|
||||
expect(result).toEqual({ frequency_penalty: 2 });
|
||||
});
|
||||
|
||||
it('should apply presence_penalty range constraints', () => {
|
||||
const result = resolveParameters(
|
||||
{ presence_penalty: -3 },
|
||||
{ presencePenaltyRange: { max: 2, min: -2 } },
|
||||
);
|
||||
expect(result).toEqual({ presence_penalty: -2 });
|
||||
});
|
||||
|
||||
it('should apply max_tokens range constraints', () => {
|
||||
const result = resolveParameters({ max_tokens: 100_000 }, { maxTokensRange: { max: 8192 } });
|
||||
expect(result).toEqual({ max_tokens: 8192 });
|
||||
});
|
||||
|
||||
it('should handle all parameters together', () => {
|
||||
const result = resolveParameters(
|
||||
{
|
||||
frequency_penalty: 0.5,
|
||||
max_tokens: 2000,
|
||||
presence_penalty: 0.5,
|
||||
temperature: 1,
|
||||
top_p: 0.9,
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(result).toEqual({
|
||||
frequency_penalty: 0.5,
|
||||
max_tokens: 2000,
|
||||
presence_penalty: 0.5,
|
||||
temperature: 0.5,
|
||||
top_p: 0.9,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world scenarios', () => {
|
||||
it('should handle Claude Opus 4.1 scenario (conflict with normalization)', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 1, top_p: 0.9 },
|
||||
{ hasConflict: true, normalizeTemperature: true, preferTemperature: true },
|
||||
);
|
||||
expect(result).toEqual({ temperature: 0.5 });
|
||||
});
|
||||
|
||||
it('should handle Zhipu glm-4-alltools scenario (range constraints)', () => {
|
||||
const result = resolveParameters(
|
||||
{ temperature: 1, top_p: 0.5 },
|
||||
{
|
||||
normalizeTemperature: true,
|
||||
temperatureRange: { max: 0.99, min: 0.01 },
|
||||
topPRange: { max: 0.99, min: 0.01 },
|
||||
},
|
||||
);
|
||||
expect(result).toEqual({ temperature: 0.5, top_p: 0.5 });
|
||||
});
|
||||
|
||||
it('should handle Groq scenario (temperature <= 0 becomes undefined)', () => {
|
||||
// In Groq's case, they handle this in their own logic, but we can test the parameter resolver
|
||||
const result = resolveParameters({ temperature: 0 }, { normalizeTemperature: false });
|
||||
expect(result.temperature).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle Qwen range constraint scenario with multiple parameters', () => {
|
||||
const result = resolveParameters(
|
||||
{ presence_penalty: 1.5, temperature: 1.5, top_p: 0.8 },
|
||||
{
|
||||
normalizeTemperature: false,
|
||||
presencePenaltyRange: { max: 2, min: -2 },
|
||||
temperatureRange: { max: 2, min: 0 },
|
||||
topPRange: { max: 1, min: 0 },
|
||||
},
|
||||
);
|
||||
expect(result).toEqual({ presence_penalty: 1.5, temperature: 1.5, top_p: 0.8 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle temperature = 0', () => {
|
||||
const result = resolveParameters({ temperature: 0 }, {});
|
||||
expect(result).toEqual({ temperature: 0 });
|
||||
});
|
||||
|
||||
it('should handle top_p = 0', () => {
|
||||
const result = resolveParameters({ top_p: 0 }, {});
|
||||
expect(result).toEqual({ top_p: 0 });
|
||||
});
|
||||
|
||||
it('should handle temperature = undefined explicitly', () => {
|
||||
const result = resolveParameters({ temperature: undefined, top_p: 0.9 }, {});
|
||||
expect(result).toEqual({ top_p: 0.9 });
|
||||
});
|
||||
|
||||
it('should handle both parameters undefined with conflict', () => {
|
||||
const result = resolveParameters({}, { hasConflict: true });
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createParameterResolver', () => {
|
||||
it('should create a resolver with predefined options', () => {
|
||||
const resolver = createParameterResolver({
|
||||
hasConflict: true,
|
||||
normalizeTemperature: true,
|
||||
preferTemperature: true,
|
||||
});
|
||||
|
||||
const result = resolver({ temperature: 1, top_p: 0.9 });
|
||||
expect(result).toEqual({ temperature: 0.5 });
|
||||
});
|
||||
|
||||
it('should create a resolver with range constraints', () => {
|
||||
const resolver = createParameterResolver({
|
||||
normalizeTemperature: true,
|
||||
temperatureRange: { max: 0.99, min: 0.01 },
|
||||
topPRange: { max: 0.99, min: 0.01 },
|
||||
});
|
||||
|
||||
const result = resolver({ temperature: 0.02, top_p: 0.005 });
|
||||
expect(result).toEqual({ temperature: 0.01, top_p: 0.01 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('MODEL_PARAMETER_CONFLICTS', () => {
|
||||
describe('ANTHROPIC_CLAUDE_4_PLUS', () => {
|
||||
it('should contain expected Claude 4+ models', () => {
|
||||
expect(MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-opus-4-1')).toBe(true);
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-opus-4-1-20250805'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-sonnet-4-5-20250929'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not contain Claude 3.x models', () => {
|
||||
expect(MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-3-opus-20240229')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-3.5-sonnet-20240620'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BEDROCK_CLAUDE_4_PLUS', () => {
|
||||
it('should contain both standard and Bedrock-specific model IDs', () => {
|
||||
expect(MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has('claude-opus-4-1')).toBe(true);
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
|
||||
'anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
|
||||
'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain all Bedrock regional variants', () => {
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
|
||||
'anthropic.claude-opus-4-20250514-v1:0',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
|
||||
'us.anthropic.claude-opus-4-20250514-v1:0',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Chat completion parameter configuration
|
||||
*/
|
||||
interface ParameterConfig {
|
||||
/**
|
||||
* Frequency penalty (reduces repetition)
|
||||
*/
|
||||
frequency_penalty?: number;
|
||||
/**
|
||||
* Maximum tokens to generate
|
||||
*/
|
||||
max_tokens?: number;
|
||||
/**
|
||||
* Presence penalty (reduces topic repetition)
|
||||
*/
|
||||
presence_penalty?: number;
|
||||
/**
|
||||
* Temperature value (0-2 range, controls randomness)
|
||||
*/
|
||||
temperature?: number;
|
||||
/**
|
||||
* Top P value (0-1 range, nucleus sampling)
|
||||
*/
|
||||
top_p?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Range constraint for a numeric parameter
|
||||
*/
|
||||
interface RangeConstraint {
|
||||
max?: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter resolver options for model-specific constraints
|
||||
*/
|
||||
interface ParameterResolverOptions {
|
||||
/**
|
||||
* Frequency penalty range constraints
|
||||
*/
|
||||
frequencyPenaltyRange?: RangeConstraint;
|
||||
/**
|
||||
* Whether the model has a conflict between temperature and top_p
|
||||
* If true, only one parameter can be set
|
||||
* @default false
|
||||
*/
|
||||
hasConflict?: boolean;
|
||||
/**
|
||||
* Max tokens range constraints
|
||||
*/
|
||||
maxTokensRange?: RangeConstraint;
|
||||
/**
|
||||
* Whether to normalize temperature (divide by 2)
|
||||
* @default true
|
||||
*/
|
||||
normalizeTemperature?: boolean;
|
||||
/**
|
||||
* Whether to prefer temperature over top_p when both are set and there's a conflict
|
||||
* @default true
|
||||
*/
|
||||
preferTemperature?: boolean;
|
||||
/**
|
||||
* Presence penalty range constraints
|
||||
*/
|
||||
presencePenaltyRange?: RangeConstraint;
|
||||
/**
|
||||
* Temperature value range constraints
|
||||
*/
|
||||
temperatureRange?: RangeConstraint;
|
||||
/**
|
||||
* Top P value range constraints
|
||||
*/
|
||||
topPRange?: RangeConstraint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved parameters ready for API calls
|
||||
*/
|
||||
interface ResolvedParameters {
|
||||
frequency_penalty?: number;
|
||||
max_tokens?: number;
|
||||
presence_penalty?: number;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply range constraints to a numeric value
|
||||
*/
|
||||
const applyRangeConstraint = (
|
||||
value: number | undefined,
|
||||
range: RangeConstraint | undefined,
|
||||
): number | undefined => {
|
||||
if (value === undefined || !range) return value;
|
||||
|
||||
let result = value;
|
||||
if (range.min !== undefined) {
|
||||
result = Math.max(range.min, result);
|
||||
}
|
||||
if (range.max !== undefined) {
|
||||
result = Math.min(range.max, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves and normalizes chat completion parameters based on model constraints
|
||||
*
|
||||
* This is a core utility for handling model-specific parameter requirements:
|
||||
* - Parameter conflicts (e.g., Claude 4+ doesn't allow both temperature and top_p)
|
||||
* - Value normalization (e.g., temperature / 2 for some models)
|
||||
* - Range constraints (e.g., min/max values)
|
||||
*
|
||||
* @param config - The input parameter values
|
||||
* @param options - Resolution options including conflict handling and normalization rules
|
||||
* @returns Resolved parameters with only valid values
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with conflict (Claude Opus 4.1)
|
||||
* resolveParameters(
|
||||
* { temperature: 1, top_p: 0.9 },
|
||||
* { hasConflict: true, preferTemperature: true }
|
||||
* )
|
||||
* // Returns: { temperature: 0.5 } // temperature normalized and top_p omitted
|
||||
*
|
||||
* @example
|
||||
* // Without conflict
|
||||
* resolveParameters(
|
||||
* { temperature: 1, top_p: 0.9 },
|
||||
* { hasConflict: false }
|
||||
* )
|
||||
* // Returns: { temperature: 0.5, top_p: 0.9 }
|
||||
*
|
||||
* @example
|
||||
* // With range constraints (Zhipu glm-4-alltools)
|
||||
* resolveParameters(
|
||||
* { temperature: 1, top_p: 0.5 },
|
||||
* {
|
||||
* normalizeTemperature: true,
|
||||
* temperatureRange: { min: 0.01, max: 0.99 },
|
||||
* topPRange: { min: 0.01, max: 0.99 }
|
||||
* }
|
||||
* )
|
||||
* // Returns: { temperature: 0.5, top_p: 0.5 }
|
||||
*
|
||||
* @example
|
||||
* // With multiple parameters (Qwen)
|
||||
* resolveParameters(
|
||||
* { temperature: 1.5, top_p: 0.8, presence_penalty: 1.5 },
|
||||
* {
|
||||
* normalizeTemperature: false,
|
||||
* temperatureRange: { min: 0, max: 2 },
|
||||
* topPRange: { min: 0, max: 1 },
|
||||
* presencePenaltyRange: { min: -2, max: 2 }
|
||||
* }
|
||||
* )
|
||||
* // Returns: { temperature: 1.5, top_p: 0.8, presence_penalty: 1.5 }
|
||||
*/
|
||||
export const resolveParameters = (
|
||||
config: ParameterConfig,
|
||||
options: ParameterResolverOptions = {},
|
||||
): ResolvedParameters => {
|
||||
const {
|
||||
hasConflict = false,
|
||||
preferTemperature = true,
|
||||
normalizeTemperature = true,
|
||||
temperatureRange,
|
||||
topPRange,
|
||||
frequencyPenaltyRange,
|
||||
presencePenaltyRange,
|
||||
maxTokensRange,
|
||||
} = options;
|
||||
|
||||
const { temperature, top_p, frequency_penalty, presence_penalty, max_tokens } = config;
|
||||
|
||||
const result: ResolvedParameters = {};
|
||||
|
||||
// Determine which parameters are provided
|
||||
const shouldSetTemperature = temperature !== undefined;
|
||||
const shouldSetTopP = top_p !== undefined;
|
||||
|
||||
// Handle temperature and top_p conflict
|
||||
if (hasConflict) {
|
||||
if (preferTemperature && shouldSetTemperature) {
|
||||
// Set temperature only
|
||||
let finalTemp =
|
||||
normalizeTemperature && temperature !== undefined ? temperature / 2 : temperature;
|
||||
result.temperature = applyRangeConstraint(finalTemp, temperatureRange);
|
||||
} else if (shouldSetTopP) {
|
||||
// Set top_p only
|
||||
result.top_p = applyRangeConstraint(top_p, topPRange);
|
||||
}
|
||||
} else {
|
||||
// No conflict: set both parameters if provided
|
||||
if (shouldSetTemperature) {
|
||||
let finalTemp =
|
||||
normalizeTemperature && temperature !== undefined ? temperature / 2 : temperature;
|
||||
result.temperature = applyRangeConstraint(finalTemp, temperatureRange);
|
||||
}
|
||||
|
||||
if (shouldSetTopP) {
|
||||
result.top_p = applyRangeConstraint(top_p, topPRange);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other parameters (no conflicts)
|
||||
if (frequency_penalty !== undefined) {
|
||||
result.frequency_penalty = applyRangeConstraint(frequency_penalty, frequencyPenaltyRange);
|
||||
}
|
||||
|
||||
if (presence_penalty !== undefined) {
|
||||
result.presence_penalty = applyRangeConstraint(presence_penalty, presencePenaltyRange);
|
||||
}
|
||||
|
||||
if (max_tokens !== undefined) {
|
||||
result.max_tokens = applyRangeConstraint(max_tokens, maxTokensRange);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a parameter resolver with predefined model-specific rules
|
||||
*
|
||||
* @example
|
||||
* // Create a resolver for Claude Opus 4.1
|
||||
* const claudeOpusResolver = createParameterResolver({
|
||||
* hasConflict: true,
|
||||
* preferTemperature: true,
|
||||
* normalizeTemperature: true
|
||||
* });
|
||||
*
|
||||
* const params = claudeOpusResolver({ temperature: 1, top_p: 0.9 });
|
||||
* // Returns: { temperature: 0.5 }
|
||||
*/
|
||||
export const createParameterResolver = (options: ParameterResolverOptions) => {
|
||||
return (config: ParameterConfig): ResolvedParameters => {
|
||||
return resolveParameters(config, options);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Common model sets that have parameter conflicts
|
||||
*/
|
||||
export const MODEL_PARAMETER_CONFLICTS = {
|
||||
/**
|
||||
* Claude models after Opus 4.1 that don't allow both temperature and top_p
|
||||
*/
|
||||
ANTHROPIC_CLAUDE_4_PLUS: new Set([
|
||||
'claude-opus-4-1',
|
||||
'claude-opus-4-1-20250805',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
]),
|
||||
|
||||
/**
|
||||
* Bedrock Claude 4+ models (including Bedrock-specific model IDs)
|
||||
*/
|
||||
BEDROCK_CLAUDE_4_PLUS: new Set([
|
||||
'claude-opus-4-1',
|
||||
'claude-opus-4-1-20250805',
|
||||
'claude-opus-4-20250514',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
// Bedrock model IDs
|
||||
'anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
'us.anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
'anthropic.claude-opus-4-20250514-v1:0',
|
||||
'us.anthropic.claude-opus-4-20250514-v1:0',
|
||||
'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
'anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
]),
|
||||
};
|
||||
+2
-6
@@ -1,12 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as desensitizeTool from '../utils/desensitizeUrl';
|
||||
import {
|
||||
CloudflareStreamTransformer,
|
||||
desensitizeCloudflareUrl,
|
||||
fillUrl,
|
||||
} from './cloudflareHelpers';
|
||||
import * as desensitizeTool from '../../utils/desensitizeUrl';
|
||||
import { CloudflareStreamTransformer, desensitizeCloudflareUrl, fillUrl } from './cloudflare';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { desensitizeUrl } from '../utils/desensitizeUrl';
|
||||
import { desensitizeUrl } from '../../utils/desensitizeUrl';
|
||||
|
||||
class CloudflareStreamTransformer {
|
||||
private textDecoder = new TextDecoder();
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './core/BaseAI';
|
||||
export { pruneReasoningPayload } from './core/contextBuilders/openai';
|
||||
export { ModelRuntime } from './core/ModelRuntime';
|
||||
export { createOpenAICompatibleRuntime } from './core/openaiCompatibleFactory';
|
||||
export * from './core/RouterRuntime';
|
||||
@@ -36,5 +37,4 @@ export * from './types/error';
|
||||
export { AgentRuntimeError } from './utils/createError';
|
||||
export { getModelPropertyWithFallback } from './utils/getFallbackModelProperty';
|
||||
export { getModelPricing } from './utils/getModelPricing';
|
||||
export { pruneReasoningPayload } from './utils/openaiHelpers';
|
||||
export { parseDataUri } from './utils/uriParser';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
import { buildAnthropicMessages } from '../../core/contextBuilders/anthropic';
|
||||
import { GenerateObjectOptions, GenerateObjectPayload } from '../../types';
|
||||
import { buildAnthropicMessages } from '../../utils/anthropicHelpers';
|
||||
|
||||
/**
|
||||
* Generate structured output using Anthropic Claude API with Function Calling
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ChatCompletionTool, ChatStreamPayload } from '@lobechat/model-runtime';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as anthropicHelpers from '../../utils/anthropicHelpers';
|
||||
import * as anthropicHelpers from '../../core/contextBuilders/anthropic';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import { LobeAnthropicAI } from './index';
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import Anthropic, { ClientOptions } from '@anthropic-ai/sdk';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { LobeRuntimeAI } from '../../core/BaseAI';
|
||||
import { buildAnthropicMessages, buildAnthropicTools } from '../../core/contextBuilders/anthropic';
|
||||
import { MODEL_PARAMETER_CONFLICTS, resolveParameters } from '../../core/parameterResolver';
|
||||
import { AnthropicStream } from '../../core/streams';
|
||||
import {
|
||||
type ChatCompletionErrorPayload,
|
||||
@@ -11,7 +13,6 @@ import {
|
||||
GenerateObjectPayload,
|
||||
} from '../../types';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { buildAnthropicMessages, buildAnthropicTools } from '../../utils/anthropicHelpers';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { debugStream } from '../../utils/debugStream';
|
||||
import { desensitizeUrl } from '../../utils/desensitizeUrl';
|
||||
@@ -31,13 +32,6 @@ type anthropicTools = Anthropic.Tool | Anthropic.WebSearchTool20250305;
|
||||
|
||||
const modelsWithSmallContextWindow = new Set(['claude-3-opus-20240229', 'claude-3-haiku-20240307']);
|
||||
|
||||
// models after Opus 4.1 that don't allow both temperature and top_p parameters
|
||||
const modelsWithTempAndTopPConflict = new Set([
|
||||
'claude-opus-4-1',
|
||||
'claude-opus-4-1-20250805',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
]);
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://api.anthropic.com';
|
||||
const DEFAULT_CACHE_TTL = '5m' as const;
|
||||
|
||||
@@ -254,9 +248,12 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
||||
} satisfies Anthropic.MessageCreateParams;
|
||||
}
|
||||
|
||||
// For Opus 4.1 models, we can only set either temperature OR top_p, not both
|
||||
const isTempAndTopPConflict = modelsWithTempAndTopPConflict.has(model);
|
||||
const shouldSetTemperature = payload.temperature !== undefined;
|
||||
// Resolve temperature and top_p parameters based on model constraints
|
||||
const hasConflict = MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has(model);
|
||||
const resolvedParams = resolveParameters(
|
||||
{ temperature, top_p },
|
||||
{ hasConflict, normalizeTemperature: true, preferTemperature: true },
|
||||
);
|
||||
|
||||
return {
|
||||
// claude 3 series model hax max output token of 4096, 3.x series has 8192
|
||||
@@ -265,17 +262,9 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
||||
messages: postMessages,
|
||||
model,
|
||||
system: systemPrompts,
|
||||
// For Opus 4.1 models: prefer temperature over top_p if both are provided
|
||||
temperature: isTempAndTopPConflict
|
||||
? shouldSetTemperature
|
||||
? temperature / 2
|
||||
: undefined
|
||||
: payload.temperature !== undefined
|
||||
? temperature / 2
|
||||
: undefined,
|
||||
temperature: resolvedParams.temperature,
|
||||
tools: postTools,
|
||||
// For Opus 4.1 models: only set top_p if temperature is not set
|
||||
top_p: isTempAndTopPConflict ? (shouldSetTemperature ? undefined : top_p) : top_p,
|
||||
top_p: resolvedParams.top_p,
|
||||
} satisfies Anthropic.MessageCreateParams;
|
||||
}
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ describe('LobeAzureOpenAI', () => {
|
||||
.spyOn(instance['client'].images, 'edit')
|
||||
.mockResolvedValue({ data: [{ url }] } as any);
|
||||
|
||||
const helpers = await import('../../utils/openaiHelpers');
|
||||
const helpers = await import('../../core/contextBuilders/openai');
|
||||
vi.spyOn(helpers, 'convertImageUrlToFile').mockResolvedValue({} as any);
|
||||
|
||||
const res = await instance.createImage({
|
||||
@@ -462,7 +462,7 @@ describe('LobeAzureOpenAI', () => {
|
||||
.spyOn(instance['client'].images, 'edit')
|
||||
.mockResolvedValue({ data: [{ url }] } as any);
|
||||
|
||||
const helpers = await import('../../utils/openaiHelpers');
|
||||
const helpers = await import('../../core/contextBuilders/openai');
|
||||
const spy = vi.spyOn(helpers, 'convertImageUrlToFile').mockResolvedValue({} as any);
|
||||
|
||||
await instance.createImage({
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Stream } from 'openai/streaming';
|
||||
|
||||
import { systemToUserModels } from '../../const/models';
|
||||
import { LobeRuntimeAI } from '../../core/BaseAI';
|
||||
import { convertImageUrlToFile, convertOpenAIMessages } from '../../core/contextBuilders/openai';
|
||||
import { transformResponseToStream } from '../../core/openaiCompatibleFactory';
|
||||
import { OpenAIStream } from '../../core/streams';
|
||||
import {
|
||||
@@ -18,7 +19,6 @@ import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { debugStream } from '../../utils/debugStream';
|
||||
import { convertImageUrlToFile, convertOpenAIMessages } from '../../utils/openaiHelpers';
|
||||
import { StreamingResponse } from '../../utils/response';
|
||||
import { sanitizeError } from '../../utils/sanitizeError';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ChatModelCard } from '@lobechat/types';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { ChatStreamPayload } from '../../types';
|
||||
|
||||
export interface BaichuanModelCard {
|
||||
@@ -31,11 +32,12 @@ export const LobeBaichuanAI = createOpenAICompatibleRuntime({
|
||||
]
|
||||
: tools;
|
||||
|
||||
// Resolve parameters with normalization
|
||||
const resolvedParams = resolveParameters({ temperature }, { normalizeTemperature: true });
|
||||
|
||||
return {
|
||||
...rest,
|
||||
// [baichuan] frequency_penalty must be between 1 and 2.
|
||||
frequency_penalty: undefined,
|
||||
temperature: temperature !== undefined ? temperature / 2 : undefined,
|
||||
temperature: resolvedParams.temperature,
|
||||
tools: baichuanTools,
|
||||
} as any;
|
||||
},
|
||||
|
||||
@@ -258,6 +258,173 @@ describe('LobeBedrockAI', () => {
|
||||
delete process.env.DEBUG_BEDROCK_CHAT_COMPLETION;
|
||||
});
|
||||
|
||||
describe('Parameter conflict handling for Claude 4+ models', () => {
|
||||
it('should send only temperature for Claude 4+ models when both temperature and top_p are provided', async () => {
|
||||
// Arrange
|
||||
const mockStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('Hello, world!');
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const mockResponse = Promise.resolve(mockStream);
|
||||
(instance['client'].send as Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
|
||||
accept: 'application/json',
|
||||
body: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: 4096,
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
temperature: 0.4, // temperature / 2, top_p omitted due to conflict
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
modelId: 'anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send only top_p for Claude 4+ models when temperature is not provided', async () => {
|
||||
// Arrange
|
||||
const mockStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('Hello, world!');
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const mockResponse = Promise.resolve(mockStream);
|
||||
(instance['client'].send as Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
top_p: 0.9,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
|
||||
accept: 'application/json',
|
||||
body: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: 4096,
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
top_p: 0.9, // temperature omitted since not provided
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
modelId: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send both temperature and top_p for non-Claude-4+ models', async () => {
|
||||
// Arrange
|
||||
const mockStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('Hello, world!');
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const mockResponse = Promise.resolve(mockStream);
|
||||
(instance['client'].send as Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
|
||||
accept: 'application/json',
|
||||
body: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: 4096,
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
temperature: 0.4, // temperature / 2
|
||||
top_p: 0.9, // both parameters allowed for older models
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle US region Claude 4+ model IDs correctly', async () => {
|
||||
// Arrange
|
||||
const mockStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('Hello, world!');
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const mockResponse = Promise.resolve(mockStream);
|
||||
(instance['client'].send as Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
temperature: 0.6,
|
||||
top_p: 0.8,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
|
||||
accept: 'application/json',
|
||||
body: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: 4096,
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
temperature: 0.3, // temperature / 2, top_p omitted due to conflict
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
modelId: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle standard Claude 4+ model IDs (non-Bedrock format)', async () => {
|
||||
// Arrange
|
||||
const mockStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('Hello, world!');
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const mockResponse = Promise.resolve(mockStream);
|
||||
(instance['client'].send as Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'claude-opus-4-1',
|
||||
temperature: 0.7,
|
||||
top_p: 0.95,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(InvokeModelWithResponseStreamCommand).toHaveBeenCalledWith({
|
||||
accept: 'application/json',
|
||||
body: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: 4096,
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
temperature: 0.35, // temperature / 2, top_p omitted due to conflict
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
modelId: 'claude-opus-4-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors and throw AgentRuntimeError', async () => {
|
||||
// Arrange
|
||||
const errorMessage = 'An error occurred';
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { LobeRuntimeAI } from '../../core/BaseAI';
|
||||
import { buildAnthropicMessages, buildAnthropicTools } from '../../core/contextBuilders/anthropic';
|
||||
import { MODEL_PARAMETER_CONFLICTS, resolveParameters } from '../../core/parameterResolver';
|
||||
import {
|
||||
AWSBedrockClaudeStream,
|
||||
AWSBedrockLlamaStream,
|
||||
@@ -19,7 +21,6 @@ import {
|
||||
EmbeddingsPayload,
|
||||
} from '../../types';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { buildAnthropicMessages, buildAnthropicTools } from '../../utils/anthropicHelpers';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { debugStream } from '../../utils/debugStream';
|
||||
import { StreamingResponse } from '../../utils/response';
|
||||
@@ -151,6 +152,13 @@ export class LobeBedrockAI implements LobeRuntimeAI {
|
||||
const system_message = messages.find((m) => m.role === 'system');
|
||||
const user_messages = messages.filter((m) => m.role !== 'system');
|
||||
|
||||
// Resolve temperature and top_p parameters based on model constraints
|
||||
const hasConflict = MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(model);
|
||||
const resolvedParams = resolveParameters(
|
||||
{ temperature, top_p },
|
||||
{ hasConflict, normalizeTemperature: true, preferTemperature: true },
|
||||
);
|
||||
|
||||
const command = new InvokeModelWithResponseStreamCommand({
|
||||
accept: 'application/json',
|
||||
body: JSON.stringify({
|
||||
@@ -158,9 +166,9 @@ export class LobeBedrockAI implements LobeRuntimeAI {
|
||||
max_tokens: max_tokens || 4096,
|
||||
messages: await buildAnthropicMessages(user_messages),
|
||||
system: system_message?.content as string,
|
||||
temperature: temperature / 2,
|
||||
temperature: resolvedParams.temperature,
|
||||
tools: buildAnthropicTools(tools),
|
||||
top_p: top_p,
|
||||
top_p: resolvedParams.top_p,
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
modelId: model,
|
||||
|
||||
@@ -3,14 +3,14 @@ import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { LobeRuntimeAI } from '../../core/BaseAI';
|
||||
import { createCallbacksTransformer } from '../../core/streams';
|
||||
import { ChatMethodOptions, ChatStreamPayload } from '../../types';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import {
|
||||
CloudflareStreamTransformer,
|
||||
DEFAULT_BASE_URL_PREFIX,
|
||||
desensitizeCloudflareUrl,
|
||||
fillUrl,
|
||||
} from '../../utils/cloudflareHelpers';
|
||||
} from '../../core/streams/cloudflare';
|
||||
import { ChatMethodOptions, ChatStreamPayload } from '../../types';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { debugStream } from '../../utils/debugStream';
|
||||
import { StreamingResponse } from '../../utils/response';
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ChatModelCard } from '@lobechat/types';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
|
||||
export interface CohereModelCard {
|
||||
context_length: number;
|
||||
@@ -18,17 +19,20 @@ export const LobeCohereAI = createOpenAICompatibleRuntime({
|
||||
handlePayload: (payload) => {
|
||||
const { frequency_penalty, presence_penalty, top_p, ...rest } = payload;
|
||||
|
||||
// Resolve parameters with range constraints
|
||||
const resolvedParams = resolveParameters(
|
||||
{ frequency_penalty, presence_penalty, top_p },
|
||||
{
|
||||
frequencyPenaltyRange: { max: 1, min: 0 },
|
||||
normalizeTemperature: false,
|
||||
presencePenaltyRange: { max: 1, min: 0 },
|
||||
topPRange: { max: 1, min: 0 },
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
frequency_penalty:
|
||||
frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 1
|
||||
? frequency_penalty
|
||||
: undefined,
|
||||
presence_penalty:
|
||||
presence_penalty !== undefined && presence_penalty > 0 && presence_penalty <= 1
|
||||
? presence_penalty
|
||||
: undefined,
|
||||
top_p: top_p !== undefined && top_p > 0 && top_p < 1 ? top_p : undefined,
|
||||
...resolvedParams,
|
||||
} as any;
|
||||
},
|
||||
noUserId: true,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { pruneReasoningPayload } from '../../core/contextBuilders/openai';
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { processMultiProviderModelList } from '../../utils/modelParse';
|
||||
import { pruneReasoningPayload } from '../../utils/openaiHelpers';
|
||||
|
||||
export interface GithubModelCard {
|
||||
capabilities: string[];
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ChatModelCard } from '@lobechat/types';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
|
||||
export interface GroqModelCard {
|
||||
@@ -19,11 +20,17 @@ export const LobeGroq = createOpenAICompatibleRuntime({
|
||||
},
|
||||
handlePayload: (payload) => {
|
||||
const { temperature, ...restPayload } = payload;
|
||||
|
||||
// Groq doesn't support temperature <= 0, set to undefined in that case
|
||||
const resolvedParams = resolveParameters({ temperature }, { normalizeTemperature: false });
|
||||
|
||||
return {
|
||||
...restPayload,
|
||||
stream: payload.stream ?? true,
|
||||
|
||||
temperature: temperature <= 0 ? undefined : temperature,
|
||||
temperature:
|
||||
resolvedParams.temperature !== undefined && resolvedParams.temperature <= 0
|
||||
? undefined
|
||||
: resolvedParams.temperature,
|
||||
} as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { minimax as minimaxChatModels , ModelProvider } from 'model-bank';
|
||||
import { ModelProvider, minimax as minimaxChatModels } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { createMiniMaxImage } from './createImage';
|
||||
|
||||
export const getMinimaxMaxOutputs = (modelId: string): number | undefined => {
|
||||
@@ -23,14 +24,31 @@ export const LobeMinimaxAI = createOpenAICompatibleRuntime({
|
||||
]
|
||||
: tools;
|
||||
|
||||
// Resolve parameters with constraints
|
||||
const resolvedParams = resolveParameters(
|
||||
{
|
||||
max_tokens: max_tokens !== undefined ? max_tokens : getMinimaxMaxOutputs(payload.model),
|
||||
temperature,
|
||||
top_p,
|
||||
},
|
||||
{
|
||||
normalizeTemperature: true,
|
||||
topPRange: { max: 1, min: 0 },
|
||||
},
|
||||
);
|
||||
|
||||
// Minimax doesn't support temperature <= 0
|
||||
const finalTemperature =
|
||||
resolvedParams.temperature !== undefined && resolvedParams.temperature <= 0
|
||||
? undefined
|
||||
: resolvedParams.temperature;
|
||||
|
||||
return {
|
||||
...params,
|
||||
frequency_penalty: undefined,
|
||||
max_tokens: max_tokens !== undefined ? max_tokens : getMinimaxMaxOutputs(payload.model),
|
||||
presence_penalty: undefined,
|
||||
temperature: temperature === undefined || temperature <= 0 ? undefined : temperature / 2,
|
||||
max_tokens: resolvedParams.max_tokens,
|
||||
temperature: finalTemperature,
|
||||
tools: minimaxTools,
|
||||
top_p: top_p !== undefined && top_p > 0 && top_p <= 1 ? top_p : undefined,
|
||||
top_p: resolvedParams.top_p,
|
||||
} as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ChatModelCard } from '@lobechat/types';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
|
||||
export interface MistralModelCard {
|
||||
capabilities: {
|
||||
@@ -19,15 +20,21 @@ export const LobeMistralAI = createOpenAICompatibleRuntime({
|
||||
// Mistral API does not support stream_options: { include_usage: true }
|
||||
// refs: https://github.com/lobehub/lobe-chat/issues/6825
|
||||
excludeUsage: true,
|
||||
handlePayload: (payload) => ({
|
||||
...(payload.max_tokens !== undefined && { max_tokens: payload.max_tokens }),
|
||||
messages: payload.messages as any,
|
||||
model: payload.model,
|
||||
stream: true,
|
||||
temperature: payload.temperature !== undefined ? payload.temperature / 2 : undefined,
|
||||
...(payload.tools && { tools: payload.tools }),
|
||||
top_p: payload.top_p,
|
||||
}),
|
||||
handlePayload: (payload) => {
|
||||
// Resolve parameters with normalization
|
||||
const resolvedParams = resolveParameters(
|
||||
{ max_tokens: payload.max_tokens, temperature: payload.temperature, top_p: payload.top_p },
|
||||
{ normalizeTemperature: true },
|
||||
);
|
||||
|
||||
return {
|
||||
...resolvedParams,
|
||||
messages: payload.messages as any,
|
||||
model: payload.model,
|
||||
stream: true,
|
||||
...(payload.tools && { tools: payload.tools }),
|
||||
};
|
||||
},
|
||||
noUserId: true,
|
||||
},
|
||||
debug: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { ChatStreamPayload } from '../../types';
|
||||
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
|
||||
|
||||
@@ -34,10 +35,13 @@ export const LobeMoonshotAI = createOpenAICompatibleRuntime({
|
||||
]
|
||||
: tools;
|
||||
|
||||
// Resolve parameters with normalization
|
||||
const resolvedParams = resolveParameters({ temperature }, { normalizeTemperature: true });
|
||||
|
||||
return {
|
||||
...rest,
|
||||
messages: filteredMessages,
|
||||
temperature: temperature !== undefined ? temperature / 2 : undefined,
|
||||
temperature: resolvedParams.temperature,
|
||||
tools: moonshotTools,
|
||||
} as any;
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { responsesAPIModels } from '../../const/models';
|
||||
import { pruneReasoningPayload } from '../../core/contextBuilders/openai';
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { ChatStreamPayload } from '../../types';
|
||||
import { processMultiProviderModelList } from '../../utils/modelParse';
|
||||
import { pruneReasoningPayload } from '../../utils/openaiHelpers';
|
||||
|
||||
export interface OpenAIModelCard {
|
||||
id: string;
|
||||
@@ -43,10 +43,10 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
|
||||
frequency_penalty: undefined,
|
||||
model,
|
||||
presence_penalty: undefined,
|
||||
...(enableServiceTierFlex && supportsFlexTier(model) && { service_tier: 'flex' }),
|
||||
stream: payload.stream ?? true,
|
||||
temperature: undefined,
|
||||
top_p: undefined,
|
||||
...(enableServiceTierFlex && supportsFlexTier(model) && { service_tier: 'flex' }),
|
||||
...(oaiSearchContextSize && {
|
||||
web_search_options: {
|
||||
search_context_size: oaiSearchContextSize,
|
||||
@@ -81,14 +81,14 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
|
||||
|
||||
const openaiTools = enabledSearch
|
||||
? [
|
||||
...(tools || []),
|
||||
{
|
||||
type: 'web_search',
|
||||
...(oaiSearchContextSize && {
|
||||
search_context_size: oaiSearchContextSize,
|
||||
}),
|
||||
},
|
||||
]
|
||||
...(tools || []),
|
||||
{
|
||||
type: 'web_search',
|
||||
...(oaiSearchContextSize && {
|
||||
search_context_size: oaiSearchContextSize,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: tools;
|
||||
|
||||
if (prunePrefixes.some((prefix) => model.startsWith(prefix))) {
|
||||
|
||||
@@ -2,31 +2,39 @@ import { ModelProvider } from 'model-bank';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { ChatStreamPayload } from '../../types';
|
||||
|
||||
export const LobePerplexityAI = createOpenAICompatibleRuntime({
|
||||
baseURL: 'https://api.perplexity.ai',
|
||||
chatCompletion: {
|
||||
handlePayload: (payload: ChatStreamPayload) => {
|
||||
// Set a default frequency penalty value greater than 0
|
||||
const { presence_penalty, frequency_penalty, stream = true, temperature, ...res } = payload;
|
||||
|
||||
let param;
|
||||
// Resolve parameters with constraints
|
||||
const resolvedParams = resolveParameters(
|
||||
{
|
||||
frequency_penalty: presence_penalty !== 0 ? undefined : frequency_penalty || 1,
|
||||
presence_penalty: presence_penalty !== 0 ? presence_penalty : undefined,
|
||||
temperature,
|
||||
},
|
||||
{
|
||||
normalizeTemperature: false,
|
||||
},
|
||||
);
|
||||
|
||||
// Ensure we are only have one frequency_penalty or frequency_penalty
|
||||
if (presence_penalty !== 0) {
|
||||
param = { presence_penalty };
|
||||
} else {
|
||||
const defaultFrequencyPenalty = 1;
|
||||
|
||||
param = { frequency_penalty: frequency_penalty || defaultFrequencyPenalty };
|
||||
}
|
||||
// Perplexity doesn't support temperature >= 2
|
||||
const finalTemperature =
|
||||
resolvedParams.temperature !== undefined && resolvedParams.temperature >= 2
|
||||
? undefined
|
||||
: resolvedParams.temperature;
|
||||
|
||||
return {
|
||||
...res,
|
||||
...param,
|
||||
frequency_penalty: resolvedParams.frequency_penalty,
|
||||
presence_penalty: resolvedParams.presence_penalty,
|
||||
stream,
|
||||
temperature: temperature >= 2 ? undefined : temperature,
|
||||
temperature: finalTemperature,
|
||||
} as OpenAI.ChatCompletionCreateParamsStreaming;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { QwenAIStream } from '../../core/streams';
|
||||
import { processMultiProviderModelList } from '../../utils/modelParse';
|
||||
import { createQwenImage } from './createImage';
|
||||
@@ -28,49 +29,46 @@ export const LobeQwenAI = createOpenAICompatibleRuntime({
|
||||
const { model, presence_penalty, temperature, thinking, top_p, enabledSearch, ...rest } =
|
||||
payload;
|
||||
|
||||
// Resolve parameters with model-specific constraints
|
||||
const resolvedParams = resolveParameters(
|
||||
{ presence_penalty, temperature, top_p },
|
||||
{
|
||||
normalizeTemperature: false,
|
||||
presencePenaltyRange: QwenLegacyModels.has(model) ? undefined : { max: 2, min: -2 },
|
||||
temperatureRange: { max: 2, min: 0 },
|
||||
topPRange:
|
||||
model.startsWith('qvq') || model.startsWith('qwen-vl')
|
||||
? { max: 1, min: 0 }
|
||||
: { max: 1, min: 0 },
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(model.includes('-thinking')
|
||||
? {
|
||||
enable_thinking: true,
|
||||
thinking_budget:
|
||||
thinking?.budget_tokens === 0 ? 0 : thinking?.budget_tokens || undefined,
|
||||
}
|
||||
: ['qwen3', 'qwen-turbo', 'qwen-plus', 'deepseek-v3.1'].some((keyword) =>
|
||||
model.toLowerCase().includes(keyword),
|
||||
)
|
||||
? {
|
||||
enable_thinking: thinking !== undefined ? thinking.type === 'enabled' : false,
|
||||
enable_thinking: true,
|
||||
thinking_budget:
|
||||
thinking?.budget_tokens === 0 ? 0 : thinking?.budget_tokens || undefined,
|
||||
}
|
||||
: ['qwen3', 'qwen-turbo', 'qwen-plus', 'deepseek-v3.1'].some((keyword) =>
|
||||
model.toLowerCase().includes(keyword),
|
||||
)
|
||||
? {
|
||||
enable_thinking: thinking !== undefined ? thinking.type === 'enabled' : false,
|
||||
thinking_budget:
|
||||
thinking?.budget_tokens === 0 ? 0 : thinking?.budget_tokens || undefined,
|
||||
}
|
||||
: {}),
|
||||
frequency_penalty: undefined,
|
||||
model,
|
||||
presence_penalty: QwenLegacyModels.has(model)
|
||||
? undefined
|
||||
: presence_penalty !== undefined && presence_penalty >= -2 && presence_penalty <= 2
|
||||
? presence_penalty
|
||||
: undefined,
|
||||
presence_penalty: resolvedParams.presence_penalty,
|
||||
stream: true,
|
||||
temperature:
|
||||
temperature !== undefined && temperature >= 0 && temperature < 2
|
||||
? temperature
|
||||
: undefined,
|
||||
...(model.startsWith('qvq') || model.startsWith('qwen-vl')
|
||||
? {
|
||||
top_p: top_p !== undefined && top_p > 0 && top_p <= 1 ? top_p : undefined,
|
||||
}
|
||||
: {
|
||||
top_p: top_p !== undefined && top_p > 0 && top_p < 1 ? top_p : undefined,
|
||||
}),
|
||||
temperature: resolvedParams.temperature,
|
||||
top_p: resolvedParams.top_p,
|
||||
...(enabledSearch && {
|
||||
enable_search: enabledSearch,
|
||||
search_options: {
|
||||
/*
|
||||
enable_citation: true,
|
||||
enable_source: true,
|
||||
*/
|
||||
search_strategy: process.env.QWEN_SEARCH_STRATEGY || 'standard', // standard or pro
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -29,7 +29,7 @@ export const LobeSearch1API = createOpenAICompatibleRuntime({
|
||||
...res,
|
||||
...param,
|
||||
stream,
|
||||
temperature: temperature >= 2 ? undefined : temperature,
|
||||
temperature: temperature !== undefined && temperature >= 2 ? undefined : temperature,
|
||||
} as OpenAI.ChatCompletionCreateParamsStreaming;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ChatModelCard } from '@lobechat/types';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { convertSenseNovaMessage } from '../../core/contextBuilders/sensenova';
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { convertSenseNovaMessage } from '../../utils/sensenovaHelpers';
|
||||
|
||||
export interface SenseNovaModelCard {
|
||||
id: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { resolveParameters } from '../../core/parameterResolver';
|
||||
import { OpenAIStream } from '../../core/streams/openai';
|
||||
import { convertIterableToStream } from '../../core/streams/protocol';
|
||||
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
|
||||
@@ -33,30 +34,32 @@ export const LobeZhipuAI = createOpenAICompatibleRuntime({
|
||||
]
|
||||
: tools;
|
||||
|
||||
// Resolve parameters based on model-specific constraints
|
||||
const resolvedParams = resolveParameters(
|
||||
{ max_tokens, temperature, top_p },
|
||||
{
|
||||
// max_tokens constraints
|
||||
maxTokensRange: model.includes('glm-4v')
|
||||
? { max: 1024 }
|
||||
: model === 'glm-zero-preview'
|
||||
? { max: 15_300 }
|
||||
: undefined,
|
||||
normalizeTemperature: true,
|
||||
// glm-4-alltools has stricter temperature and top_p constraints
|
||||
...(model === 'glm-4-alltools' && {
|
||||
temperatureRange: { max: 0.99, min: 0.01 },
|
||||
topPRange: { max: 0.99, min: 0.01 },
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
max_tokens:
|
||||
max_tokens === undefined
|
||||
? undefined
|
||||
: (model.includes('glm-4v') && Math.min(max_tokens, 1024)) ||
|
||||
(model === 'glm-zero-preview' && Math.min(max_tokens, 15_300)) ||
|
||||
max_tokens,
|
||||
...resolvedParams,
|
||||
model,
|
||||
stream: true,
|
||||
thinking: model.includes('-4.5') ? { type: thinking?.type } : undefined,
|
||||
tools: zhipuTools,
|
||||
...(model === 'glm-4-alltools'
|
||||
? {
|
||||
temperature:
|
||||
temperature !== undefined
|
||||
? Math.max(0.01, Math.min(0.99, temperature / 2))
|
||||
: undefined,
|
||||
top_p: top_p !== undefined ? Math.max(0.01, Math.min(0.99, top_p)) : undefined,
|
||||
}
|
||||
: {
|
||||
temperature: temperature !== undefined ? temperature / 2 : undefined,
|
||||
top_p,
|
||||
}),
|
||||
} as any;
|
||||
},
|
||||
handleStream: (stream, { callbacks, inputStartAt }) => {
|
||||
|
||||
@@ -108,7 +108,7 @@ export interface ChatStreamPayload {
|
||||
* @title 生成文本的随机度量,用于控制文本的创造性和多样性
|
||||
* @default 1
|
||||
*/
|
||||
temperature: number;
|
||||
temperature?: number;
|
||||
text?: {
|
||||
verbosity?: 'low' | 'medium' | 'high';
|
||||
};
|
||||
|
||||
@@ -196,8 +196,6 @@ export const chatTopic: StateCreator<
|
||||
enable ? [SWR_USE_FETCH_TOPIC, sessionId] : null,
|
||||
async ([, sessionId]: [string, string]) => topicService.getTopics({ sessionId }),
|
||||
{
|
||||
suspense: true,
|
||||
fallbackData: [],
|
||||
onSuccess: (topics) => {
|
||||
const nextMap = { ...get().topicMaps, [sessionId]: topics };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user