Merge pull request #581 from alibaba/refactor/llms
refactor(llms): rewrite model patching and deprecate temperature
This commit is contained in:
Vendored
+1
@@ -53,6 +53,7 @@
|
|||||||
"Ollama",
|
"Ollama",
|
||||||
"onwarn",
|
"onwarn",
|
||||||
"opensource",
|
"opensource",
|
||||||
|
"openrouter",
|
||||||
"pageagent",
|
"pageagent",
|
||||||
"pageagentcore",
|
"pageagentcore",
|
||||||
"pageagentext",
|
"pageagentext",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default defineConfig([
|
|||||||
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
|
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
|
||||||
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
|
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
|
||||||
'@typescript-eslint/require-await': 'off',
|
'@typescript-eslint/require-await': 'off',
|
||||||
|
'@typescript-eslint/no-deprecated': 'off',
|
||||||
'@eslint-react/dom-no-missing-button-type': 'off',
|
'@eslint-react/dom-no-missing-button-type': 'off',
|
||||||
'@eslint-react/no-nested-component-definitions': 'off',
|
'@eslint-react/no-nested-component-definitions': 'off',
|
||||||
'@eslint-react/no-array-index-key': 'off',
|
'@eslint-react/no-array-index-key': 'off',
|
||||||
|
|||||||
@@ -4,17 +4,24 @@
|
|||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorTypes } from './errors'
|
import { InvokeError, InvokeErrorTypes } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type {
|
||||||
|
InvokeOptions,
|
||||||
|
InvokeResult,
|
||||||
|
LLMClient,
|
||||||
|
Message,
|
||||||
|
ResolvedLLMConfig,
|
||||||
|
Tool,
|
||||||
|
} from './types'
|
||||||
import { modelPatch, zodToOpenAITool } from './utils'
|
import { modelPatch, zodToOpenAITool } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client for OpenAI compatible APIs
|
* Client for OpenAI compatible APIs
|
||||||
*/
|
*/
|
||||||
export class OpenAIClient implements LLMClient {
|
export class OpenAIClient implements LLMClient {
|
||||||
config: Required<LLMConfig>
|
config: ResolvedLLMConfig
|
||||||
private fetch: typeof globalThis.fetch
|
private fetch: typeof globalThis.fetch
|
||||||
|
|
||||||
constructor(config: Required<LLMConfig>) {
|
constructor(config: ResolvedLLMConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.fetch = config.customFetch
|
this.fetch = config.customFetch
|
||||||
}
|
}
|
||||||
@@ -39,14 +46,18 @@ export class OpenAIClient implements LLMClient {
|
|||||||
|
|
||||||
const requestBody: Record<string, unknown> = {
|
const requestBody: Record<string, unknown> = {
|
||||||
model: this.config.model,
|
model: this.config.model,
|
||||||
temperature: this.config.temperature,
|
|
||||||
messages,
|
messages,
|
||||||
tools: openaiTools,
|
tools: openaiTools,
|
||||||
parallel_tool_calls: false,
|
parallel_tool_calls: false,
|
||||||
tool_choice: toolChoice,
|
tool_choice: toolChoice,
|
||||||
}
|
}
|
||||||
|
// Only sent if the caller explicitly set it. Most new models throw if this is set.
|
||||||
|
if (this.config.temperature !== undefined) {
|
||||||
|
requestBody.temperature = this.config.temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
modelPatch(requestBody, this.config.baseURL)
|
||||||
|
|
||||||
modelPatch(requestBody)
|
|
||||||
let transformedBody: Record<string, unknown> | undefined
|
let transformedBody: Record<string, unknown> | undefined
|
||||||
try {
|
try {
|
||||||
transformedBody = this.config.transformRequestBody(requestBody)
|
transformedBody = this.config.transformRequestBody(requestBody)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// Internal constants
|
|
||||||
export const LLM_MAX_RETRIES = 2
|
|
||||||
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
|
|
||||||
+41
-24
@@ -1,34 +1,23 @@
|
|||||||
import { OpenAIClient } from './OpenAIClient'
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
|
||||||
import { InvokeError, InvokeErrorTypes } from './errors'
|
import { InvokeError, InvokeErrorTypes } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type {
|
||||||
|
InvokeOptions,
|
||||||
|
InvokeResult,
|
||||||
|
LLMClient,
|
||||||
|
LLMConfig,
|
||||||
|
Message,
|
||||||
|
ResolvedLLMConfig,
|
||||||
|
Tool,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
export { InvokeError, InvokeErrorTypes }
|
export { InvokeError, InvokeErrorTypes }
|
||||||
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
||||||
|
|
||||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
/**
|
||||||
// Runtime validation as defensive programming (types already guarantee these)
|
* LLM module
|
||||||
if (!config.baseURL || !config.model) {
|
*/
|
||||||
throw new Error(
|
|
||||||
'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
|
|
||||||
'See: https://alibaba.github.io/page-agent/docs/features/models'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseURL: config.baseURL,
|
|
||||||
model: config.model,
|
|
||||||
apiKey: config.apiKey || '',
|
|
||||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
|
||||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
|
||||||
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
|
||||||
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
|
||||||
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LLM extends EventTarget {
|
export class LLM extends EventTarget {
|
||||||
config: Required<LLMConfig>
|
config: ResolvedLLMConfig
|
||||||
client: LLMClient
|
client: LLMClient
|
||||||
|
|
||||||
constructor(config: LLMConfig) {
|
constructor(config: LLMConfig) {
|
||||||
@@ -90,3 +79,31 @@ async function withRetry<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseLLMConfig(config: LLMConfig): ResolvedLLMConfig {
|
||||||
|
// Runtime validation as defensive programming (types already guarantee these)
|
||||||
|
if (!config.baseURL || !config.model) {
|
||||||
|
throw new Error(
|
||||||
|
'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
|
||||||
|
'See: https://alibaba.github.io/page-agent/docs/features/models'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.temperature !== undefined) {
|
||||||
|
console.warn(
|
||||||
|
'[PageAgent] LLMConfig.temperature is deprecated and will be removed in a future version. ' +
|
||||||
|
'Use transformRequestBody to set it only for models you have verified accept it.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
model: config.model,
|
||||||
|
apiKey: config.apiKey || '',
|
||||||
|
temperature: config.temperature,
|
||||||
|
maxRetries: config.maxRetries ?? 2,
|
||||||
|
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
||||||
|
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
||||||
|
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
/**
|
||||||
|
* Live compatibility test — hits real provider APIs.
|
||||||
|
*
|
||||||
|
* Purpose: verify request formatting and `modelPatch` (see `utils.ts`) work
|
||||||
|
* for every model below, across the providers that serve them. Not a
|
||||||
|
* correctness/quality eval — the tool call is trivial and forced via
|
||||||
|
* `toolChoiceName`, so a failure here means the request/response shape is
|
||||||
|
* wrong for that model, not that the model is "dumb".
|
||||||
|
*
|
||||||
|
* Tests `OpenAIClient` directly (not the `LLM` retry wrapper), so a failure
|
||||||
|
* always reflects the very first request/response — no retry can mask it.
|
||||||
|
*
|
||||||
|
* Each provider's tests skip (not fail) when its `TESTING_*_KEY` env var is
|
||||||
|
* absent, so this stays CI-safe. To actually run these, put keys in the
|
||||||
|
* repo-root `.env`:
|
||||||
|
*
|
||||||
|
* TESTING_OPENROUTER_KEY=...
|
||||||
|
* TESTING_DEEPSEEK_KEY=...
|
||||||
|
* TESTING_ALIYUN_KEY=...
|
||||||
|
*/
|
||||||
|
import { config as dotenvConfig } from 'dotenv'
|
||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
|
import { parseLLMConfig } from './index'
|
||||||
|
import type { Message, Tool } from './types'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
dotenvConfig({ path: resolve(__dirname, '../../../.env'), quiet: true })
|
||||||
|
|
||||||
|
const TEST_TIMEOUT = 30_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors `packages/website/src/pages/docs/features/models/page.tsx`.
|
||||||
|
* This package cannot depend on the website, so the list is duplicated here.
|
||||||
|
* Keep both lists in sync manually when models are added or renamed.
|
||||||
|
*/
|
||||||
|
const MODEL_GROUPS: Record<string, string[]> = {
|
||||||
|
Qwen: [
|
||||||
|
'qwen3.7-max',
|
||||||
|
'qwen3.7-plus',
|
||||||
|
'qwen3.6-max',
|
||||||
|
'qwen3.6-plus',
|
||||||
|
'qwen3.6-flash',
|
||||||
|
'qwen3.5-plus',
|
||||||
|
'qwen3.5-flash',
|
||||||
|
'qwen3-max',
|
||||||
|
],
|
||||||
|
OpenAI: [
|
||||||
|
'gpt-5.5',
|
||||||
|
'gpt-5.4',
|
||||||
|
'gpt-5.4-mini',
|
||||||
|
'gpt-5.4-nano',
|
||||||
|
'gpt-5.2',
|
||||||
|
'gpt-5.1',
|
||||||
|
'gpt-5',
|
||||||
|
'gpt-5-mini',
|
||||||
|
'gpt-4.1',
|
||||||
|
'gpt-4.1-mini',
|
||||||
|
],
|
||||||
|
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
|
||||||
|
Google: [
|
||||||
|
'gemini-3.5-flash',
|
||||||
|
'gemini-3.1-pro',
|
||||||
|
'gemini-3.1-flash-lite',
|
||||||
|
'gemini-2.5-pro',
|
||||||
|
'gemini-2.5-flash',
|
||||||
|
],
|
||||||
|
Anthropic: [
|
||||||
|
'claude-sonnet-5',
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-7',
|
||||||
|
'claude-opus-4-6',
|
||||||
|
'claude-opus-4-5',
|
||||||
|
'claude-sonnet-4-5',
|
||||||
|
'claude-haiku-4-5',
|
||||||
|
],
|
||||||
|
MiniMax: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.5'],
|
||||||
|
xAI: ['grok-4.3', 'grok-build-0.1'],
|
||||||
|
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
|
||||||
|
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenRouter lists every model as `<vendor-slug>/<model-id>`, lowercase.
|
||||||
|
* See the commented-out entries in the repo-root `.env` for real examples,
|
||||||
|
* e.g. `x-ai/grok-4.1-fast`, `qwen/qwen3-coder-next`, `deepseek/deepseek-v3.2-exp`.
|
||||||
|
*/
|
||||||
|
const OPENROUTER_VENDOR_SLUG: Record<string, string> = {
|
||||||
|
Qwen: 'qwen',
|
||||||
|
OpenAI: 'openai',
|
||||||
|
DeepSeek: 'deepseek',
|
||||||
|
Google: 'google',
|
||||||
|
Anthropic: 'anthropic',
|
||||||
|
MiniMax: 'minimax',
|
||||||
|
xAI: 'x-ai',
|
||||||
|
MoonshotAI: 'moonshotai',
|
||||||
|
'Z.AI': 'z-ai',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides for models whose OpenRouter id doesn't match the
|
||||||
|
* `<vendor-slug>/<lowercased-name>` heuristic — dated snapshots, "-preview"
|
||||||
|
* suffixes, "v"-prefixed versions, or dots instead of hyphens in the
|
||||||
|
* version number. Verified against `GET https://openrouter.ai/api/v1/models`
|
||||||
|
* on 2026-07-03; re-check when models are added to `MODEL_GROUPS`.
|
||||||
|
*/
|
||||||
|
const OPENROUTER_ID_OVERRIDES: Record<string, string> = {
|
||||||
|
'qwen3.6-max': 'qwen/qwen3.6-max-preview',
|
||||||
|
'qwen3.5-plus': 'qwen/qwen3.5-plus-20260420',
|
||||||
|
'qwen3.5-flash': 'qwen/qwen3.5-flash-02-23',
|
||||||
|
'deepseek-3.2': 'deepseek/deepseek-v3.2',
|
||||||
|
'gemini-3.1-pro': 'google/gemini-3.1-pro-preview',
|
||||||
|
'claude-opus-4-8': 'anthropic/claude-opus-4.8',
|
||||||
|
'claude-opus-4-7': 'anthropic/claude-opus-4.7',
|
||||||
|
'claude-opus-4-6': 'anthropic/claude-opus-4.6',
|
||||||
|
'claude-opus-4-5': 'anthropic/claude-opus-4.5',
|
||||||
|
'claude-sonnet-4-5': 'anthropic/claude-sonnet-4.5',
|
||||||
|
'claude-haiku-4-5': 'anthropic/claude-haiku-4.5',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a model's OpenRouter id from its brand (a `MODEL_GROUPS` key) and
|
||||||
|
* native model name.
|
||||||
|
*/
|
||||||
|
function toOpenRouterModelId(brand: string, model: string): string {
|
||||||
|
if (model in OPENROUTER_ID_OVERRIDES) return OPENROUTER_ID_OVERRIDES[model]
|
||||||
|
const slug = OPENROUTER_VENDOR_SLUG[brand]
|
||||||
|
if (!slug) throw new Error(`No OpenRouter vendor slug mapped for brand "${brand}"`)
|
||||||
|
return `${slug}/${model.toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROVIDERS = {
|
||||||
|
openrouter: {
|
||||||
|
baseURL: 'https://openrouter.ai/api/v1',
|
||||||
|
apiKey: process.env.TESTING_OPENROUTER_KEY,
|
||||||
|
},
|
||||||
|
aliyun: {
|
||||||
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
|
apiKey: process.env.TESTING_ALIYUN_KEY,
|
||||||
|
},
|
||||||
|
deepseek: {
|
||||||
|
baseURL: 'https://api.deepseek.com',
|
||||||
|
apiKey: process.env.TESTING_DEEPSEEK_KEY,
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const ECHO_TOOL: Tool<{ message: string }, string> = {
|
||||||
|
description: 'Echo back the given message in uppercase.',
|
||||||
|
inputSchema: z.object({ message: z.string() }),
|
||||||
|
execute: async ({ message }) => message.toUpperCase(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROMPT: Message[] = [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: 'Call the "echo" tool with message set to "ping". You must call the tool.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function expectEchoToolCall(baseURL: string, apiKey: string, model: string) {
|
||||||
|
const client = new OpenAIClient(parseLLMConfig({ baseURL, apiKey, model }))
|
||||||
|
const result = await client.invoke(PROMPT, { echo: ECHO_TOOL }, new AbortController().signal, {
|
||||||
|
toolChoiceName: 'echo',
|
||||||
|
})
|
||||||
|
expect(result.toolResult).toBe('PING')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.concurrent('OpenRouter — all listed models', () => {
|
||||||
|
const { baseURL, apiKey } = PROVIDERS.openrouter
|
||||||
|
|
||||||
|
for (const [brand, models] of Object.entries(MODEL_GROUPS)) {
|
||||||
|
for (const model of models) {
|
||||||
|
const id = toOpenRouterModelId(brand, model)
|
||||||
|
it.skipIf(!apiKey)(
|
||||||
|
id,
|
||||||
|
async () => {
|
||||||
|
await expectEchoToolCall(baseURL, apiKey!, id)
|
||||||
|
},
|
||||||
|
TEST_TIMEOUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Aliyun native ids that don't match the display name in MODEL_GROUPS.
|
||||||
|
const ALIYUN_ID_OVERRIDES: Record<string, string> = {
|
||||||
|
'qwen3.6-max': 'qwen3.6-max-preview',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.concurrent('Aliyun DashScope — Qwen native', () => {
|
||||||
|
const { baseURL, apiKey } = PROVIDERS.aliyun
|
||||||
|
|
||||||
|
for (const model of MODEL_GROUPS.Qwen) {
|
||||||
|
const id = ALIYUN_ID_OVERRIDES[model] ?? model
|
||||||
|
it.skipIf(!apiKey)(
|
||||||
|
id,
|
||||||
|
async () => {
|
||||||
|
await expectEchoToolCall(baseURL, apiKey!, id)
|
||||||
|
},
|
||||||
|
TEST_TIMEOUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe.concurrent('DeepSeek — native', () => {
|
||||||
|
const { baseURL, apiKey } = PROVIDERS.deepseek
|
||||||
|
// deepseek-3.2 isn't served on DeepSeek's own API (only via OpenRouter
|
||||||
|
// resellers) — its official API only accepts deepseek-v4-pro/-flash.
|
||||||
|
const nativeModels = MODEL_GROUPS.DeepSeek.filter((model) => model !== 'deepseek-3.2')
|
||||||
|
|
||||||
|
for (const model of nativeModels) {
|
||||||
|
it.skipIf(!apiKey)(
|
||||||
|
model,
|
||||||
|
async () => {
|
||||||
|
await expectEchoToolCall(baseURL, apiKey!, model)
|
||||||
|
},
|
||||||
|
TEST_TIMEOUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -92,7 +92,12 @@ export interface LLMConfig {
|
|||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated No longer a standard parameter; many models reject it outright.
|
||||||
|
* Use `transformRequestBody` to set it only for models you've verified.
|
||||||
|
*/
|
||||||
temperature?: number
|
temperature?: number
|
||||||
|
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,3 +123,5 @@ export interface LLMConfig {
|
|||||||
*/
|
*/
|
||||||
customFetch?: typeof globalThis.fetch
|
customFetch?: typeof globalThis.fetch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResolvedLLMConfig = Required<Omit<LLMConfig, 'temperature'>> & { temperature?: number }
|
||||||
|
|||||||
+10
-157
@@ -1,162 +1,15 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { modelPatch } from './utils'
|
import { modelPatch, normalizeModelName } from './utils'
|
||||||
|
|
||||||
/**
|
describe('normalizeModelName', () => {
|
||||||
* Baseline request body used as starting point for each provider test.
|
it.each([
|
||||||
* Mirrors what OpenAIClient builds before calling modelPatch.
|
['gpt-5.2', 'gpt-52'],
|
||||||
*/
|
['gpt_5_2', 'gpt52'],
|
||||||
function baseBody(model: string) {
|
['GPT-52-2026-01-01', 'gpt-52-2026-01-01'],
|
||||||
return {
|
['openai/gpt-5.2-chat', 'gpt-52-chat'],
|
||||||
model,
|
['claude_sonnet4_5', 'claudesonnet45'],
|
||||||
temperature: 0.7,
|
])('%s -> %s', (input, expected) => {
|
||||||
messages: [],
|
expect(normalizeModelName(input)).toBe(expected)
|
||||||
tools: [],
|
|
||||||
parallel_tool_calls: false,
|
|
||||||
tool_choice: 'required' as unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('modelPatch', () => {
|
|
||||||
it('returns body unchanged when model is missing', () => {
|
|
||||||
const body = { temperature: 0.7 }
|
|
||||||
expect(modelPatch(body)).toBe(body)
|
|
||||||
expect(body).toEqual({ temperature: 0.7 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('qwen: bumps temperature and disables thinking', () => {
|
|
||||||
const body = baseBody('qwen-max')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body.temperature).toBe(1.0)
|
|
||||||
expect(body).toMatchObject({ enable_thinking: false })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('qwen: keeps higher caller-provided temperature', () => {
|
|
||||||
const body = baseBody('qwen-max')
|
|
||||||
body.temperature = 1.5
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body.temperature).toBe(1.5)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('claude: disables thinking and converts tool_choice "required" -> { type: "any" }', () => {
|
|
||||||
const body = baseBody('claude-3-5-sonnet')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
thinking: { type: 'disabled' },
|
|
||||||
tool_choice: { type: 'any' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('claude: converts named tool_choice to { type: "tool", name }', () => {
|
|
||||||
const body = baseBody('claude-3-5-sonnet')
|
|
||||||
body.tool_choice = { type: 'function', function: { name: 'doStuff' } }
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body.tool_choice).toEqual({ type: 'tool', name: 'doStuff' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('claude-opus-4-7: drops temperature', () => {
|
|
||||||
const body = baseBody('claude-opus-4-7')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).not.toHaveProperty('temperature')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('claude-opus-47 (alt id form): drops temperature', () => {
|
|
||||||
// Provider sometimes ships ids with the dot stripped; modelPatch normalizes.
|
|
||||||
const body = baseBody('claude-opus-47-20251029')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).not.toHaveProperty('temperature')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('claude-opus-4-8: drops temperature', () => {
|
|
||||||
const body = baseBody('claude-opus-4-8')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).not.toHaveProperty('temperature')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('claude-opus-48 (alt id form): drops temperature', () => {
|
|
||||||
const body = baseBody('claude-opus-48-20251210')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).not.toHaveProperty('temperature')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('grok: removes tool_choice and disables reasoning/thinking', () => {
|
|
||||||
const body = baseBody('grok-4')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).not.toHaveProperty('tool_choice')
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
thinking: { type: 'disabled', effort: 'minimal' },
|
|
||||||
reasoning: { enabled: false, effort: 'low' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gpt-5: sets verbosity=low and reasoning_effort=low', () => {
|
|
||||||
const body = baseBody('gpt-5')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({ verbosity: 'low', reasoning_effort: 'low' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gpt-5-mini: low effort, temperature=1', () => {
|
|
||||||
const body = baseBody('gpt-5-mini')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
verbosity: 'low',
|
|
||||||
reasoning_effort: 'low',
|
|
||||||
temperature: 1,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gpt-5.1 (gpt-51): disables reasoning', () => {
|
|
||||||
const body = baseBody('gpt-5.1')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({ verbosity: 'low', reasoning_effort: 'none' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gpt-5.4 (gpt-54): drops reasoning_effort', () => {
|
|
||||||
const body = baseBody('gpt-5.4')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({ verbosity: 'low' })
|
|
||||||
expect(body).not.toHaveProperty('reasoning_effort')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gpt-5.5 (gpt-55): drops reasoning_effort and temperature', () => {
|
|
||||||
const body = baseBody('gpt-5.5')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({ verbosity: 'low' })
|
|
||||||
expect(body).not.toHaveProperty('reasoning_effort')
|
|
||||||
expect(body).not.toHaveProperty('temperature')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gemini: sets reasoning_effort=minimal', () => {
|
|
||||||
const body = baseBody('gemini-2.5-pro')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({ reasoning_effort: 'minimal' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('deepseek: removes tool_choice', () => {
|
|
||||||
const body = baseBody('deepseek-chat')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).not.toHaveProperty('tool_choice')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('minimax: clamps temperature into (0, 1] and removes parallel_tool_calls', () => {
|
|
||||||
const body = baseBody('minimax-m2')
|
|
||||||
body.temperature = 0
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body.temperature).toBeGreaterThan(0)
|
|
||||||
expect(body.temperature).toBeLessThanOrEqual(1)
|
|
||||||
expect(body).not.toHaveProperty('parallel_tool_calls')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('minimax: caps temperature at 1', () => {
|
|
||||||
const body = baseBody('minimax-m2')
|
|
||||||
body.temperature = 2
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body.temperature).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('normalizes provider-prefixed model id (openai/gpt-5)', () => {
|
|
||||||
const body = baseBody('openai/gpt-5')
|
|
||||||
modelPatch(body)
|
|
||||||
expect(body).toMatchObject({ verbosity: 'low', reasoning_effort: 'low' })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+122
-64
@@ -24,25 +24,64 @@ export function zodToOpenAITool(name: string, tool: Tool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch model specific parameters
|
* Patch model specific parameters. Only patches known models.
|
||||||
* @note in-place modification
|
*
|
||||||
|
* @purpose
|
||||||
|
* - Reconcile the differences in the parameter schema each model accepts.
|
||||||
|
* - Disable thinking/reasoning, or lower it to the minimum where a full disable is impossible.
|
||||||
|
* - Minimize returned tokens.
|
||||||
|
* - Raise temperature for known smaller models to improve auto-recovery odds.
|
||||||
|
* @note Honor temperature if explicitly set by the user
|
||||||
|
*
|
||||||
|
* @todo Need vendor-specific patches.
|
||||||
|
* Local and 3rd-party hosted models may have different schema.
|
||||||
*/
|
*/
|
||||||
export function modelPatch(body: Record<string, any>) {
|
export function modelPatch(body: Record<string, any>, baseURL?: string) {
|
||||||
const model: string = body.model || ''
|
const model: string = body.model || ''
|
||||||
if (!model) return body
|
if (!model) return body
|
||||||
|
|
||||||
|
const provider = getProvider(baseURL)
|
||||||
|
|
||||||
const modelName = normalizeModelName(model)
|
const modelName = normalizeModelName(model)
|
||||||
|
|
||||||
if (modelName.startsWith('qwen')) {
|
if (modelName.startsWith('qwen')) {
|
||||||
debug('Applying Qwen patch: use higher temperature for auto fixing')
|
debug('Patch Qwen: disable thinking')
|
||||||
body.temperature = Math.max(body.temperature || 0, 1.0)
|
|
||||||
body.enable_thinking = false
|
body.enable_thinking = false
|
||||||
|
if (body.temperature === undefined && !/max|plus/.test(modelName)) {
|
||||||
|
debug('Patch Qwen: raise temperature to 1.0')
|
||||||
|
body.temperature = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelName.startsWith('deepseek')) {
|
||||||
|
debug('Patch DeepSeek: disable thinking, remove tool_choice')
|
||||||
|
body.thinking = { type: 'disabled' }
|
||||||
|
delete body.tool_choice
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelName.startsWith('gpt')) {
|
||||||
|
if (modelName.startsWith('gpt-5')) {
|
||||||
|
body.verbosity = 'low'
|
||||||
|
|
||||||
|
// gpt-5 gpt-5-mini gpt-5-nano only supports "minimal";
|
||||||
|
// 5.1+ supports "none".
|
||||||
|
body.reasoning_effort = /^gpt-5(-|$)/.test(modelName) ? 'minimal' : 'none'
|
||||||
|
debug(`Patch GPT-5: verbosity=low, reasoning_effort=${body.reasoning_effort}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelName.includes('chat-latest')) {
|
||||||
|
debug('Omitting reasoning_effort and temperature for chat-latest')
|
||||||
|
delete body.reasoning_effort
|
||||||
|
delete body.temperature
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('claude')) {
|
if (modelName.startsWith('claude')) {
|
||||||
debug('Applying Claude patch: disable thinking')
|
if (/opus|sonnet|haiku/.test(modelName)) {
|
||||||
|
debug('Patch Claude: disable thinking')
|
||||||
body.thinking = { type: 'disabled' }
|
body.thinking = { type: 'disabled' }
|
||||||
|
|
||||||
|
if (provider !== 'openrouter') {
|
||||||
// Convert tool_choice to Claude format
|
// Convert tool_choice to Claude format
|
||||||
if (body.tool_choice === 'required') {
|
if (body.tool_choice === 'required') {
|
||||||
// 'required' -> { type: 'any' } (must call some tool)
|
// 'required' -> { type: 'any' } (must call some tool)
|
||||||
@@ -53,77 +92,84 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
debug('Applying Claude patch: convert tool_choice format')
|
debug('Applying Claude patch: convert tool_choice format')
|
||||||
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
|
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Claude naming pattern has changed
|
|
||||||
// needs proper handling
|
|
||||||
if (
|
|
||||||
modelName.startsWith('claude-opus-4-7') ||
|
|
||||||
modelName.startsWith('claude-opus-47') ||
|
|
||||||
modelName.startsWith('claude-opus-4-8') ||
|
|
||||||
modelName.startsWith('claude-opus-48')
|
|
||||||
) {
|
|
||||||
debug('Applying Claude-4.7/4.8 patch: remove temperature')
|
|
||||||
delete body.temperature
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debug('Patch Claude: reasoning_effort=low')
|
||||||
|
body.reasoning_effort = 'low'
|
||||||
|
|
||||||
if (modelName.startsWith('grok')) {
|
// Fable and mythos can not disable adaptive thinking.
|
||||||
debug('Applying Grok patch: removing tool_choice')
|
// Claude does not support tool_choice with extended thinking.
|
||||||
|
// These 2 concepts are blurred. Basically no tool_choice with thinking.
|
||||||
delete body.tool_choice
|
delete body.tool_choice
|
||||||
debug('Applying Grok patch: disable reasoning and thinking')
|
|
||||||
body.thinking = { type: 'disabled', effort: 'minimal' }
|
|
||||||
body.reasoning = { enabled: false, effort: 'low' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelName.startsWith('gpt')) {
|
|
||||||
debug('Applying GPT patch: set verbosity to low')
|
|
||||||
body.verbosity = 'low'
|
|
||||||
|
|
||||||
// *-chat-latest models don't support reasoning_effort — skip patches that set it
|
|
||||||
if (modelName.includes('chat-latest')) {
|
|
||||||
debug('Omitting reasoning_effort and temperature for chat-latest')
|
|
||||||
delete body.reasoning_effort
|
|
||||||
delete body.temperature
|
|
||||||
} else if (modelName.startsWith('gpt-52')) {
|
|
||||||
debug('Applying GPT-52 patch: disable reasoning')
|
|
||||||
body.reasoning_effort = 'none'
|
|
||||||
} else if (modelName.startsWith('gpt-51')) {
|
|
||||||
debug('Applying GPT-51 patch: disable reasoning')
|
|
||||||
body.reasoning_effort = 'none'
|
|
||||||
} else if (modelName.startsWith('gpt-54')) {
|
|
||||||
debug('Applying GPT-5.4 patch: remove reasoning_effort')
|
|
||||||
delete body.reasoning_effort
|
|
||||||
} else if (modelName.startsWith('gpt-55')) {
|
|
||||||
debug('Applying GPT-5.4 patch: remove reasoning_effort and temperature')
|
|
||||||
delete body.reasoning_effort
|
|
||||||
delete body.temperature
|
|
||||||
} else if (modelName.startsWith('gpt-5-mini')) {
|
|
||||||
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
|
||||||
body.reasoning_effort = 'low'
|
|
||||||
body.temperature = 1
|
|
||||||
} else if (modelName.startsWith('gpt-5')) {
|
|
||||||
debug('Applying GPT-5 patch: set reasoning effort to low')
|
|
||||||
body.reasoning_effort = 'low'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('gemini')) {
|
if (modelName.startsWith('gemini')) {
|
||||||
debug('Applying Gemini patch: set reasoning effort to minimal')
|
debug('Patch Gemini: reasoning_effort=low')
|
||||||
|
body.reasoning_effort = 'low'
|
||||||
|
if (/^gemini-25(?!.*pro)/.test(modelName)) {
|
||||||
|
debug('Patch Gemini 2.5 non-Pro: reasoning_effort=none')
|
||||||
|
body.reasoning_effort = 'none'
|
||||||
|
} else if (
|
||||||
|
modelName.startsWith('gemini-35-flash') ||
|
||||||
|
modelName.startsWith('gemini-31-flash-lite') ||
|
||||||
|
modelName.startsWith('gemini-3-flash')
|
||||||
|
) {
|
||||||
|
debug('Patch Gemini 3.x Flash/Lite: reasoning_effort=minimal')
|
||||||
body.reasoning_effort = 'minimal'
|
body.reasoning_effort = 'minimal'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('deepseek')) {
|
if (modelName.startsWith('glm')) {
|
||||||
debug('Applying DeepSeek patch: remove tool_choice')
|
debug('Patch GLM: disable thinking')
|
||||||
delete body.tool_choice
|
body.thinking = { type: 'disabled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelName.startsWith('grok')) {
|
||||||
|
if (/^grok-4-?3/.test(modelName)) {
|
||||||
|
debug('Patch Grok 4.3: reasoning_effort=none')
|
||||||
|
body.reasoning_effort = 'none'
|
||||||
|
} else if (modelName.startsWith('grok-3-mini') || modelName.startsWith('grok-code-fast')) {
|
||||||
|
debug('Patch Grok mini/code: reasoning_effort=low')
|
||||||
|
body.reasoning_effort = 'low'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelName.startsWith('kimi')) {
|
||||||
|
if (!modelName.includes('code')) {
|
||||||
|
// kimi-k2.7-code cannot disable thinking
|
||||||
|
debug('Patch Kimi: disable thinking')
|
||||||
|
body.thinking = { type: 'disabled' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('minimax')) {
|
if (modelName.startsWith('minimax')) {
|
||||||
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
|
debug('Patch MiniMax: remove parallel_tool_calls')
|
||||||
// MiniMax API rejects temperature = 0; clamp to a small positive value
|
|
||||||
body.temperature = Math.max(body.temperature || 0, 0.01)
|
|
||||||
if (body.temperature > 1) body.temperature = 1
|
|
||||||
// MiniMax does not support parallel_tool_calls
|
|
||||||
delete body.parallel_tool_calls
|
delete body.parallel_tool_calls
|
||||||
|
|
||||||
|
if (modelName.includes('m3')) {
|
||||||
|
// Only M3 can disable thinking
|
||||||
|
debug('Patch MiniMax: disable thinking')
|
||||||
|
body.thinking = { type: 'disabled' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// provider patches
|
||||||
|
|
||||||
|
if (provider === 'openrouter') {
|
||||||
|
// openrouter use reasoning object instead of reasoning_effort
|
||||||
|
|
||||||
|
const reasoningEffort = body.reasoning_effort
|
||||||
|
const reasoningDisabled =
|
||||||
|
body.thinking?.type === 'disabled' ||
|
||||||
|
body.enable_thinking === false ||
|
||||||
|
reasoningEffort === 'none'
|
||||||
|
|
||||||
|
if (reasoningDisabled) {
|
||||||
|
body.reasoning = { enabled: false }
|
||||||
|
} else if (reasoningEffort) {
|
||||||
|
body.reasoning = { enabled: true, effort: reasoningEffort }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
@@ -144,7 +190,7 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
* They should be treated as the same model.
|
* They should be treated as the same model.
|
||||||
* Normalize them to `gpt-52`
|
* Normalize them to `gpt-52`
|
||||||
*/
|
*/
|
||||||
function normalizeModelName(modelName: string): string {
|
export function normalizeModelName(modelName: string): string {
|
||||||
let normalizedName = modelName.toLowerCase()
|
let normalizedName = modelName.toLowerCase()
|
||||||
|
|
||||||
// remove prefix before '/'
|
// remove prefix before '/'
|
||||||
@@ -160,3 +206,15 @@ function normalizeModelName(modelName: string): string {
|
|||||||
|
|
||||||
return normalizedName
|
return normalizedName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProvider(baseURL?: string): 'openrouter' | undefined {
|
||||||
|
if (!baseURL) return undefined
|
||||||
|
try {
|
||||||
|
const url = new URL(baseURL)
|
||||||
|
const hostname = url.hostname
|
||||||
|
if (hostname === 'openrouter.ai') return 'openrouter'
|
||||||
|
return undefined
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,9 +208,7 @@ export async function inputTextElement(element: HTMLElement, text: string) {
|
|||||||
selection?.removeAllRanges()
|
selection?.removeAllRanges()
|
||||||
selection?.addRange(range)
|
selection?.addRange(range)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
||||||
doc.execCommand('delete', false)
|
doc.execCommand('delete', false)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
||||||
doc.execCommand('insertText', false, text)
|
doc.execCommand('insertText', false, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,13 +143,6 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
required: false,
|
required: false,
|
||||||
description: 'LLM AK',
|
description: 'LLM AK',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'temperature',
|
|
||||||
type: 'number',
|
|
||||||
description: isZh
|
|
||||||
? '模型温度参数,控制输出随机性'
|
|
||||||
: 'Model temperature, controls output randomness',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'maxRetries',
|
name: 'maxRetries',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Heading } from '@/components/Heading'
|
|||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
const BASELINE = new Set([
|
const BASELINE = new Set([
|
||||||
'gpt-5.1',
|
|
||||||
'gpt-5.4-mini',
|
'gpt-5.4-mini',
|
||||||
'claude-haiku-4.5',
|
'gpt-5.4-nano',
|
||||||
|
'claude-haiku-4-5',
|
||||||
'gemini-3.5-flash',
|
'gemini-3.5-flash',
|
||||||
'deepseek-v4-flash',
|
'deepseek-v4-flash',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
@@ -25,9 +25,8 @@ const MODEL_GROUPS: Record<string, string[]> = {
|
|||||||
'qwen3.6-flash',
|
'qwen3.6-flash',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
'qwen3.5-flash',
|
'qwen3.5-flash',
|
||||||
'qwen3-coder-next',
|
'qwen3-max',
|
||||||
'qwen-3-max',
|
// 'qwen3-coder-next', // low success rate
|
||||||
'qwen-3-plus',
|
|
||||||
],
|
],
|
||||||
OpenAI: [
|
OpenAI: [
|
||||||
'gpt-5.5',
|
'gpt-5.5',
|
||||||
@@ -44,24 +43,29 @@ const MODEL_GROUPS: Record<string, string[]> = {
|
|||||||
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
|
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
|
||||||
Google: [
|
Google: [
|
||||||
'gemini-3.5-flash',
|
'gemini-3.5-flash',
|
||||||
|
'gemini-3.1-pro',
|
||||||
'gemini-3.1-flash-lite',
|
'gemini-3.1-flash-lite',
|
||||||
'gemini-3-pro',
|
'gemini-2.5-pro',
|
||||||
'gemini-3-flash',
|
'gemini-2.5-flash',
|
||||||
'gemini-2.5',
|
|
||||||
],
|
],
|
||||||
Anthropic: [
|
Anthropic: [
|
||||||
'claude-opus-4.8',
|
'claude-sonnet-5',
|
||||||
'claude-opus-4.7',
|
'claude-fable-5',
|
||||||
'claude-opus-4.6',
|
'claude-opus-4-8',
|
||||||
'claude-opus-4.5',
|
'claude-opus-4-7',
|
||||||
'claude-sonnet-4.5',
|
'claude-opus-4-6',
|
||||||
'claude-haiku-4.5',
|
'claude-opus-4-5',
|
||||||
'claude-sonnet-3.5',
|
'claude-sonnet-4-5',
|
||||||
|
'claude-haiku-4-5',
|
||||||
],
|
],
|
||||||
MiniMax: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed'],
|
MiniMax: [
|
||||||
xAI: ['grok-4.1-fast', 'grok-4', 'grok-code-fast'],
|
// 'MiniMax-M3', low success rate
|
||||||
MoonshotAI: ['kimi-k2.5'],
|
'MiniMax-M2.7',
|
||||||
'Z.AI': ['glm-5', 'glm-4.7'],
|
'MiniMax-M2.5',
|
||||||
|
],
|
||||||
|
xAI: ['grok-4.3', 'grok-build-0.1'],
|
||||||
|
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
|
||||||
|
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModelBadge = ({ model, baseline }: { model: string; baseline?: boolean }) => (
|
const ModelBadge = ({ model, baseline }: { model: string; baseline?: boolean }) => (
|
||||||
@@ -91,7 +95,7 @@ export default function Models() {
|
|||||||
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
||||||
{isZh ? '已测试模型' : 'Tested Models'}
|
{isZh ? '模型列表' : 'Model List'}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
||||||
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
|
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
|
||||||
@@ -124,8 +128,8 @@ export default function Models() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{isZh
|
{isZh
|
||||||
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature'
|
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复'
|
||||||
: 'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended'}
|
: 'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover'}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{isZh
|
{isZh
|
||||||
@@ -281,7 +285,7 @@ LLM_MODEL_NAME="qwen3.5-plus"`}
|
|||||||
code={`const pageAgent = new PageAgent({
|
code={`const pageAgent = new PageAgent({
|
||||||
baseURL: 'https://your-claude-proxy.example/v1',
|
baseURL: 'https://your-claude-proxy.example/v1',
|
||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'claude-sonnet-4.5',
|
model: 'claude-sonnet-5',
|
||||||
transformRequestBody: (requestBody) => ({
|
transformRequestBody: (requestBody) => ({
|
||||||
...requestBody,
|
...requestBody,
|
||||||
cache_control: { type: 'ephemeral' },
|
cache_control: { type: 'ephemeral' },
|
||||||
|
|||||||
Reference in New Issue
Block a user