feat: 新增模型支持并扩展功能配置

- 添加 Gemini 3.1 Flash Lite 模型到 provider-models 和别名映射
- 扩展 Codex 转换器以支持 reasoning 配置和服务层级传递
- 为 Codex 模型添加 -fast 后缀变体,自动设置优先级服务层级和高推理强度
- 扩展 Grok 模型以支持 -nsfw 后缀变体,并添加账户级 NSFW 设置流程
- 放宽 Gemini 模型思考功能检测条件以包含所有 gemini-3 系列模型
This commit is contained in:
hex2077 2026-03-08 20:20:16 +08:00
parent 4a1a382dc2
commit 3f3c9e1a45
6 changed files with 124 additions and 18 deletions

View file

@ -1 +1 @@
2.10.7 2.10.8

View file

@ -103,20 +103,19 @@ export class CodexConverter extends BaseConverter {
codexRequest.store = false; codexRequest.store = false;
codexRequest.parallel_tool_calls = true; codexRequest.parallel_tool_calls = true;
codexRequest.include = ['reasoning.encrypted_content']; codexRequest.include = ['reasoning.encrypted_content'];
codexRequest.service_tier = responsesRequest.service_tier || 'default';
// 删除Codex不支持的字段 // 删除Codex不支持的字段
delete codexRequest.max_output_tokens; delete codexRequest.max_output_tokens;
delete codexRequest.max_completion_tokens; delete codexRequest.max_completion_tokens;
delete codexRequest.temperature; delete codexRequest.temperature;
delete codexRequest.top_p; delete codexRequest.top_p;
delete codexRequest.service_tier;
delete codexRequest.user; delete codexRequest.user;
delete codexRequest.reasoning;
// 添加 reasoning 配置 // 添加 reasoning 配置
codexRequest.reasoning = { codexRequest.reasoning = {
"effort": "medium", "effort": responsesRequest.reasoning_effort || responsesRequest.reasoning?.effort || "medium",
"summary": "auto" "summary": responsesRequest.reasoning?.summary || "auto"
}; };
@ -155,11 +154,12 @@ export class CodexConverter extends BaseConverter {
store: false, store: false,
metadata: data.metadata || {}, metadata: data.metadata || {},
reasoning: { reasoning: {
effort: data.reasoning_effort || 'medium', effort: data.reasoning_effort || data.reasoning?.effort || 'medium',
summary: 'auto' summary: data.reasoning?.summary || 'auto'
}, },
parallel_tool_calls: true, parallel_tool_calls: true,
include: ['reasoning.encrypted_content'] include: ['reasoning.encrypted_content'],
service_tier: data.service_tier || 'default'
}; };
// 处理 OpenAI Responses 特有的 instructions 和 input 字段(如果存在) // 处理 OpenAI Responses 特有的 instructions 和 input 字段(如果存在)

View file

@ -63,6 +63,7 @@ const MODEL_ALIAS_MAP = {
'gemini-3-pro-image-preview': 'gemini-3-pro-image', 'gemini-3-pro-image-preview': 'gemini-3-pro-image',
'gemini-3-pro-preview': 'gemini-3-pro-high', 'gemini-3-pro-preview': 'gemini-3-pro-high',
'gemini-3.1-pro-preview': 'gemini-3.1-pro-high', 'gemini-3.1-pro-preview': 'gemini-3.1-pro-high',
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite-preview',
'gemini-3-flash-preview': 'gemini-3-flash', 'gemini-3-flash-preview': 'gemini-3-flash',
'gemini-2.5-flash-preview': 'gemini-2.5-flash', 'gemini-2.5-flash-preview': 'gemini-2.5-flash',
'gemini-claude-sonnet-4-5': 'claude-sonnet-4-5', 'gemini-claude-sonnet-4-5': 'claude-sonnet-4-5',
@ -77,6 +78,7 @@ const MODEL_NAME_MAP = {
'gemini-3-pro-image': 'gemini-3-pro-image-preview', 'gemini-3-pro-image': 'gemini-3-pro-image-preview',
'gemini-3-pro-high': 'gemini-3-pro-preview', 'gemini-3-pro-high': 'gemini-3-pro-preview',
'gemini-3.1-pro-high': 'gemini-3.1-pro-preview', 'gemini-3.1-pro-high': 'gemini-3.1-pro-preview',
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite-preview',
'gemini-3-flash': 'gemini-3-flash-preview', 'gemini-3-flash': 'gemini-3-flash-preview',
'gemini-2.5-flash': 'gemini-2.5-flash-preview', 'gemini-2.5-flash': 'gemini-2.5-flash-preview',
'claude-sonnet-4-5': 'gemini-claude-sonnet-4-5', 'claude-sonnet-4-5': 'gemini-claude-sonnet-4-5',
@ -129,8 +131,8 @@ function isImageModel(modelName) {
function modelSupportsThinking(modelName) { function modelSupportsThinking(modelName) {
if (!modelName) return false; if (!modelName) return false;
const name = modelName.toLowerCase(); const name = modelName.toLowerCase();
// 支持 thinking 的模型gemini-3-*, gemini-2.5-*, claude-*-thinking // 支持 thinking 的模型gemini-3*, gemini-2.5-*, claude-*-thinking
return name.startsWith('gemini-3-') || return name.startsWith('gemini-3') ||
name.startsWith('gemini-2.5-') || name.startsWith('gemini-2.5-') ||
name.includes('-thinking'); name.includes('-thinking');
} }

View file

@ -34,8 +34,7 @@ const httpsAgent = new https.Agent({
ALPNProtocols: ['http/1.1'], ecdhCurve: 'X25519:P-256:P-384', honorCipherOrder: false, sessionTimeout: 300, ALPNProtocols: ['http/1.1'], ecdhCurve: 'X25519:P-256:P-384', honorCipherOrder: false, sessionTimeout: 300,
}); });
const GROK_MODELS = getProviderModels(MODEL_PROVIDER.GROK_CUSTOM); const CORE_MODEL_MAPPING = {
const MODEL_MAPPING = {
'grok-3': { name: 'grok-3', mode: 'MODEL_MODE_GROK_3' }, 'grok-3': { name: 'grok-3', mode: 'MODEL_MODE_GROK_3' },
'grok-3-mini': { name: 'grok-3', mode: 'MODEL_MODE_GROK_3_MINI_THINKING' }, 'grok-3-mini': { name: 'grok-3', mode: 'MODEL_MODE_GROK_3_MINI_THINKING' },
'grok-3-thinking': { name: 'grok-3', mode: 'MODEL_MODE_GROK_3_THINKING' }, 'grok-3-thinking': { name: 'grok-3', mode: 'MODEL_MODE_GROK_3_THINKING' },
@ -53,6 +52,24 @@ const MODEL_MAPPING = {
'grok-imagine-1.0-video': { name: 'grok-3', mode: 'MODEL_MODE_FAST' } 'grok-imagine-1.0-video': { name: 'grok-3', mode: 'MODEL_MODE_FAST' }
}; };
const MODEL_MAPPING = { ...CORE_MODEL_MAPPING };
Object.keys(CORE_MODEL_MAPPING).forEach(key => {
if (!key.endsWith('-nsfw')) {
MODEL_MAPPING[`${key}-nsfw`] = CORE_MODEL_MAPPING[key];
}
});
const GROK_MODELS = Object.keys(MODEL_MAPPING);
function isGrokNsfwModel(modelId) {
return typeof modelId === 'string' && modelId.toLowerCase().endsWith('-nsfw');
}
function normalizeGrokModelId(modelId) {
if (typeof modelId !== 'string') return modelId;
return isGrokNsfwModel(modelId) ? modelId.slice(0, -5) : modelId;
}
export class GrokApiService { export class GrokApiService {
constructor(config) { constructor(config) {
this.config = config; this.config = config;
@ -63,11 +80,70 @@ export class GrokApiService {
this.baseUrl = config.GROK_BASE_URL || 'https://grok.com'; this.baseUrl = config.GROK_BASE_URL || 'https://grok.com';
this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`; this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`;
this.isInitialized = false; this.isInitialized = false;
this.nsfwSetupDone = false;
this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK); this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK);
if (this.converter && this.uuid) this.converter.setUuid(this.uuid); if (this.converter && this.uuid) this.converter.setUuid(this.uuid);
this.lastSyncAt = null; this.lastSyncAt = null;
} }
async setupNsfw() {
if (this.nsfwSetupDone) return;
try {
await this.acceptTos();
await this.setBirthDate();
await this.enableNsfwAccount();
this.nsfwSetupDone = true;
logger.info(`[Grok NSFW] Account-level NSFW setup completed for ${this.uuid}`);
} catch (error) {
logger.warn(`[Grok NSFW] Failed to setup account-level NSFW: ${error.message}`);
}
}
async acceptTos() {
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/app-chat/accept-tos`, headers: this.buildHeaders(), data: {}, httpAgent, httpsAgent, timeout: 15000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try { await axios(axiosConfig); } catch (e) { logger.debug(`[Grok TOS] ${e.message}`); }
}
async setBirthDate() {
const axiosConfig = { method: 'post', url: `${this.baseUrl}/rest/app-chat/set-birth-date`, headers: this.buildHeaders(), data: { "birthDate": "1990-01-01" }, httpAgent, httpsAgent, timeout: 15000 };
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try { await axios(axiosConfig); } catch (e) { logger.debug(`[Grok Birth] ${e.message}`); }
}
async enableNsfwAccount() {
const name = Buffer.from("always_show_nsfw_content");
const inner = Buffer.concat([Buffer.from([0x0a, name.length]), name]);
const protobuf = Buffer.concat([Buffer.from([0x0a, 0x02, 0x10, 0x01, 0x12, inner.length]), inner]);
const header = Buffer.alloc(5);
header.writeUInt8(0, 0);
header.writeUInt32BE(protobuf.length, 1);
const payload = Buffer.concat([header, protobuf]);
const headers = this.buildHeaders();
headers['content-type'] = 'application/grpc-web+proto';
headers['x-grpc-web'] = '1';
headers['x-user-agent'] = 'connect-es/2.1.1';
headers['referer'] = `${this.baseUrl}/?_s=data`;
const axiosConfig = {
method: 'post',
url: `${this.baseUrl}/auth_mgmt.AuthManagement/UpdateUserFeatureControls`,
headers,
data: payload,
httpAgent,
httpsAgent,
timeout: 15000,
responseType: 'arraybuffer'
};
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try { await axios(axiosConfig); } catch (e) { throw e; }
}
_applySidecar(axiosConfig) { _applySidecar(axiosConfig) {
const sidecar = getTLSSidecar(); const sidecar = getTLSSidecar();
if (sidecar.isReady()) { if (sidecar.isReady()) {
@ -243,7 +319,9 @@ export class GrokApiService {
} }
buildPayload(modelId, requestBody) { buildPayload(modelId, requestBody) {
const mapping = MODEL_MAPPING[modelId] || MODEL_MAPPING['grok-3']; const rawModelId = typeof modelId === 'string' ? modelId : '';
const normalizedModelId = normalizeGrokModelId(rawModelId);
const mapping = MODEL_MAPPING[normalizedModelId] || MODEL_MAPPING['grok-3'];
let message = requestBody.message || ""; let message = requestBody.message || "";
let toolOverrides = requestBody.toolOverrides || {}; let toolOverrides = requestBody.toolOverrides || {};
let fileAttachments = requestBody.fileAttachments || []; let fileAttachments = requestBody.fileAttachments || [];
@ -296,12 +374,13 @@ export class GrokApiService {
if (requestBody.videoGenPrompt) message = requestBody.videoGenPrompt; if (requestBody.videoGenPrompt) message = requestBody.videoGenPrompt;
} }
const modelLower = modelId.toLowerCase(); const modelLower = normalizedModelId.toLowerCase();
const isMediaModel = modelLower.includes('imagine') || modelLower.includes('video') || modelLower.includes('edit'); const isMediaModel = modelLower.includes('imagine') || modelLower.includes('video') || modelLower.includes('edit');
const isNsfw = isGrokNsfwModel(rawModelId) || requestBody.nsfw === true || requestBody.disableNsfwFilter === true;
return { return {
"deviceEnvInfo": { "darkModeEnabled": false, "devicePixelRatio": 2, "screenWidth": 2056, "screenHeight": 1329, "viewportWidth": 2056, "viewportHeight": 1083 }, "deviceEnvInfo": { "darkModeEnabled": false, "devicePixelRatio": 2, "screenWidth": 2056, "screenHeight": 1329, "viewportWidth": 2056, "viewportHeight": 1083 },
"disableMemory": false, "disableSearch": false, "disableSelfHarmShortCircuit": false, "disableTextFollowUps": false, "disableMemory": false, "disableNsfwFilter": isNsfw, "disableSearch": false, "disableSelfHarmShortCircuit": false, "disableTextFollowUps": false,
"enableImageGeneration": isMediaModel, "enableImageStreaming": isMediaModel, "enableSideBySide": true, "enableImageGeneration": isMediaModel, "enableImageStreaming": isMediaModel, "enableSideBySide": true,
"fileAttachments": fileAttachments, "forceConcise": false, "forceSideBySide": false, "imageAttachments": [], "imageGenerationCount": 2, "fileAttachments": fileAttachments, "forceConcise": false, "forceSideBySide": false, "imageAttachments": [], "imageGenerationCount": 2,
"isAsyncChat": false, "isReasoning": false, "message": message, "modelMode": mapping.mode, "modelName": mapping.name, "isAsyncChat": false, "isReasoning": false, "message": message, "modelMode": mapping.mode, "modelName": mapping.name,
@ -406,9 +485,14 @@ export class GrokApiService {
getProviderPoolManager().markProviderNeedRefresh(MODEL_PROVIDER.GROK_CUSTOM, { uuid: this.uuid }); getProviderPoolManager().markProviderNeedRefresh(MODEL_PROVIDER.GROK_CUSTOM, { uuid: this.uuid });
} }
const rawModel = typeof model === 'string' ? model : '';
const normalizedModel = normalizeGrokModelId(rawModel);
const modelLower = normalizedModel.toLowerCase();
const isNsfw = isGrokNsfwModel(rawModel) || requestBody.nsfw === true || requestBody.disableNsfwFilter === true;
if (isNsfw) await this.setupNsfw();
this.buildPayload(model, requestBody); this.buildPayload(model, requestBody);
const modelLower = model.toLowerCase();
const isVideoModel = modelLower.includes('video'); const isVideoModel = modelLower.includes('video');
const isImageModel = modelLower.includes('imagine') && !isVideoModel && !modelLower.includes('edit'); const isImageModel = modelLower.includes('imagine') && !isVideoModel && !modelLower.includes('edit');
const isImageEditModel = modelLower.includes('edit'); const isImageEditModel = modelLower.includes('edit');
@ -461,7 +545,8 @@ export class GrokApiService {
requestBody.toolOverrides = { ...requestBody.toolOverrides, videoGen: true }; requestBody.toolOverrides = { ...requestBody.toolOverrides, videoGen: true };
} }
} else if (isImageModel || isImageEditModel) { } else if (isImageModel || isImageEditModel) {
requestBody.toolOverrides = { ...requestBody.toolOverrides, imageGen: true }; const isNsfw = isGrokNsfwModel(rawModel) || requestBody.nsfw === true || requestBody.disableNsfwFilter === true;
requestBody.toolOverrides = { ...requestBody.toolOverrides, imageGen: isNsfw ? { disableNsfwFilter: true } : true };
} }
let fileAttachments = requestBody.fileAttachments || []; let fileAttachments = requestBody.fileAttachments || [];

View file

@ -10,7 +10,9 @@ import { MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js';
import { getProxyConfigForProvider } from '../../utils/proxy-utils.js'; import { getProxyConfigForProvider } from '../../utils/proxy-utils.js';
import { getProviderModels } from '../provider-models.js'; import { getProviderModels } from '../provider-models.js';
const CODEX_MODELS = getProviderModels(MODEL_PROVIDER.CODEX_API); const baseModels = getProviderModels(MODEL_PROVIDER.CODEX_API);
const fastModels = baseModels.map(m => `${m}-fast`);
const CODEX_MODELS = [...new Set([...baseModels, ...fastModels])];
const CODEX_VERSION = '0.111.0'; const CODEX_VERSION = '0.111.0';
/** /**
@ -339,9 +341,19 @@ export class CodexApiService {
// 明确会话维度:优先使用 session_id 或 conversation_id其次 user_id // 明确会话维度:优先使用 session_id 或 conversation_id其次 user_id
const sessionId = metadata.session_id || metadata.conversation_id || metadata.user_id || 'default'; const sessionId = metadata.session_id || metadata.conversation_id || metadata.user_id || 'default';
// 判断是否为 fast 模型并确定默认值
const isFastModel = model.endsWith('-fast');
const defaultServiceTier = isFastModel ? 'priority' : 'default';
const defaultReasoningEffort = isFastModel ? 'xhigh' : 'medium';
const cleanedBody = { ...requestBody }; const cleanedBody = { ...requestBody };
delete cleanedBody.metadata; delete cleanedBody.metadata;
// 如果是 fast 模型,移除后缀再传给上游
if (isFastModel) {
cleanedBody.model = model.replace('-fast', '');
}
// 生成会话缓存键 // 生成会话缓存键
// 弱化 model 依赖,以提升同会话跨模型的缓存命中率 // 弱化 model 依赖,以提升同会话跨模型的缓存命中率
// 仅当 sessionId 为 'default' 时加上 model 前缀,提供基础隔离 // 仅当 sessionId 为 'default' 时加上 model 前缀,提供基础隔离
@ -363,6 +375,11 @@ export class CodexApiService {
// 注意requestBody 已经去除了 metadata // 注意requestBody 已经去除了 metadata
return { return {
...cleanedBody, ...cleanedBody,
service_tier: cleanedBody.service_tier || defaultServiceTier,
reasoning: {
...cleanedBody.reasoning,
effort: cleanedBody.reasoning?.effort || defaultReasoningEffort
},
stream, stream,
prompt_cache_key: cache.id prompt_cache_key: cache.id
}; };

View file

@ -13,11 +13,13 @@ export const PROVIDER_MODELS = {
'gemini-3-pro-preview', 'gemini-3-pro-preview',
'gemini-3-flash-preview', 'gemini-3-flash-preview',
'gemini-3.1-pro-preview', 'gemini-3.1-pro-preview',
'gemini-3.1-flash-lite-preview',
], ],
'gemini-antigravity': [ 'gemini-antigravity': [
'gemini-2.5-computer-use-preview-10-2025', 'gemini-2.5-computer-use-preview-10-2025',
'gemini-3-pro-image-preview', 'gemini-3-pro-image-preview',
'gemini-3.1-pro-preview', 'gemini-3.1-pro-preview',
'gemini-3.1-flash-lite-preview',
'gemini-3-pro-preview', 'gemini-3-pro-preview',
'gemini-3-flash-preview', 'gemini-3-flash-preview',
'gemini-2.5-flash-preview', 'gemini-2.5-flash-preview',