feat: sideQuery 支持第三方 provider 路由 (OpenAI/Grok/Gemini)

- 新增 getProviderPrimaryModel() 从环境变量解析 provider 主模型
- getDefaultOpus/Sonnet/HaikuModel 在第三方 provider 下回退到用户配置的主模型
- sideQuery 根据 provider 类型分发到对应的 API 适配器
- 新增 sideQueryViaOpenAICompatible (OpenAI + Grok) 和 sideQueryViaGemini 适配函数
- 避免 sideQuery 后台任务在配置第三方端点时仍请求 Anthropic API
This commit is contained in:
claude-code-best 2026-05-31 14:08:30 +08:00 committed by James Feng
parent b9fcd991cc
commit 9ae258f917
2 changed files with 47 additions and 19 deletions

View File

@ -123,6 +123,19 @@ export function getBestModel(): ModelName {
return getDefaultOpusModel() return getDefaultOpusModel()
} }
/**
* Resolve the provider's primary model from its env var (e.g. OPENAI_MODEL).
* Returns undefined for providers that don't have a primary-model env var
* (Bedrock, Vertex, Foundry, firstParty).
*/
function getProviderPrimaryModel(): ModelName | undefined {
const provider = getAPIProvider()
if (provider === 'openai') return process.env.OPENAI_MODEL
if (provider === 'gemini') return process.env.GEMINI_MODEL
if (provider === 'grok') return process.env.GROK_MODEL
return undefined
}
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged). // @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).
export function getDefaultOpusModel(): ModelName { export function getDefaultOpusModel(): ModelName {
const provider = getAPIProvider() const provider = getAPIProvider()
@ -138,10 +151,15 @@ export function getDefaultOpusModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) { if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
} }
// Fall back to user's configured model — custom providers may not // 3P providers: if user set a primary model (e.g. OPENAI_MODEL=glm-5.1),
// recognize hardcoded Anthropic model IDs. // fall back to it instead of a hardcoded Anthropic model. This prevents
// Skip if the user setting is a model alias (e.g. "opus", "opus[1m]") to // sideQuery / background tasks from sending requests to Anthropic's API
// avoid infinite recursion: parseUserSpecifiedModel(alias) → getDefaultOpusModel(). // when the user configured a third-party provider.
const primaryModel = getProviderPrimaryModel()
if (primaryModel) return primaryModel
// Fall back to user's configured model — custom providers may not recognize
// hardcoded Anthropic model IDs. Skip aliases to avoid infinite recursion:
// parseUserSpecifiedModel(alias) → getDefaultOpusModel().
const userSpecifiedOpus = getUserSpecifiedModelSetting() const userSpecifiedOpus = getUserSpecifiedModelSetting()
if (userSpecifiedOpus && !isAliasOrAliasWithSuffix(userSpecifiedOpus)) { if (userSpecifiedOpus && !isAliasOrAliasWithSuffix(userSpecifiedOpus)) {
return parseUserSpecifiedModel(userSpecifiedOpus) return parseUserSpecifiedModel(userSpecifiedOpus)
@ -170,9 +188,12 @@ export function getDefaultSonnetModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) { if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
} }
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) — // 3P providers: fall back to user's primary model instead of a hardcoded
// custom providers (proxies, national clouds) may not recognize the // Anthropic model name. Prevents background API calls from being routed to
// hardcoded Anthropic model IDs. // Anthropic when the user configured a third-party endpoint.
const primaryModel = getProviderPrimaryModel()
if (primaryModel) return primaryModel
// Fall back to user's configured model (ANTHROPIC_MODEL / settings).
// Skip if the user setting is a model alias to avoid infinite recursion. // Skip if the user setting is a model alias to avoid infinite recursion.
const userSpecified = getUserSpecifiedModelSetting() const userSpecified = getUserSpecifiedModelSetting()
if (userSpecified && !isAliasOrAliasWithSuffix(userSpecified)) { if (userSpecified && !isAliasOrAliasWithSuffix(userSpecified)) {
@ -200,9 +221,12 @@ export function getDefaultHaikuModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
} }
// Fall back to user's configured model — custom providers may not // 3P providers: fall back to user's primary model instead of a hardcoded
// recognize hardcoded Anthropic model IDs. // Anthropic model name.
// Skip if the user setting is a model alias to avoid infinite recursion. const primaryModel = getProviderPrimaryModel()
if (primaryModel) return primaryModel
// Fall back to user's configured model — custom providers may not recognize
// hardcoded Anthropic model IDs. Skip aliases to avoid infinite recursion.
const userSpecifiedHaiku = getUserSpecifiedModelSetting() const userSpecifiedHaiku = getUserSpecifiedModelSetting()
if (userSpecifiedHaiku && !isAliasOrAliasWithSuffix(userSpecifiedHaiku)) { if (userSpecifiedHaiku && !isAliasOrAliasWithSuffix(userSpecifiedHaiku)) {
return parseUserSpecifiedModel(userSpecifiedHaiku) return parseUserSpecifiedModel(userSpecifiedHaiku)

View File

@ -35,7 +35,6 @@ import { normalizeModelStringForAPI } from './model/model.js'
import { getAPIProvider } from './model/providers.js' import { getAPIProvider } from './model/providers.js'
import { getOpenAIClient } from '../services/api/openai/client.js' import { getOpenAIClient } from '../services/api/openai/client.js'
import { getGrokClient } from '../services/api/grok/client.js' import { getGrokClient } from '../services/api/grok/client.js'
import { anthropicMessagesToOpenAI } from '../services/api/openai/convertMessages.js'
import { resolveOpenAIModel } from '../services/api/openai/modelMapping.js' import { resolveOpenAIModel } from '../services/api/openai/modelMapping.js'
import { resolveGrokModel } from '../services/api/grok/modelMapping.js' import { resolveGrokModel } from '../services/api/grok/modelMapping.js'
import { import {
@ -222,10 +221,9 @@ async function sideQueryViaOpenAICompatible(
if (openaiToolChoice) requestParams.tool_choice = openaiToolChoice if (openaiToolChoice) requestParams.tool_choice = openaiToolChoice
} }
const response = await client.chat.completions.create( const response = await client.chat.completions.create(requestParams as any, {
requestParams as any, signal,
{ signal }, })
)
const choice = response.choices[0] const choice = response.choices[0]
const message = choice?.message const message = choice?.message
@ -303,9 +301,14 @@ async function sideQueryViaOpenAICompatible(
* Use this instead of direct client.beta.messages.create() calls to ensure * Use this instead of direct client.beta.messages.create() calls to ensure
* proper OAuth token validation with fingerprint attribution headers. * proper OAuth token validation with fingerprint attribution headers.
* *
* Third-party provider routing (OpenAI, Grok, Gemini) is handled transparently * This handles:
* when the user configures a third-party provider, sideQuery automatically * - Fingerprint computation for OAuth validation
* routes to the correct API adapter instead of sending requests to Anthropic. * - Attribution header injection
* - CLI system prompt prefix
* - Proper betas for the model
* - API metadata
* - Model string normalization (strips [1m] suffix for API)
* - Third-party provider routing (OpenAI, Grok, Gemini)
* *
* @example * @example
* // Permission explainer * // Permission explainer
@ -603,7 +606,8 @@ async function sideQueryViaGemini(
const body: Record<string, unknown> = { const body: Record<string, unknown> = {
contents, contents,
...(systemInstruction && { systemInstruction }), ...(systemInstruction && { systemInstruction }),
...(geminiTools && (geminiTools as unknown[]).length > 0 && { tools: geminiTools }), ...(geminiTools &&
(geminiTools as unknown[]).length > 0 && { tools: geminiTools }),
...(geminiToolConfig && { ...(geminiToolConfig && {
toolConfig: { functionCallingConfig: geminiToolConfig }, toolConfig: { functionCallingConfig: geminiToolConfig },
}), }),