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.parallel_tool_calls = true;
codexRequest.include = ['reasoning.encrypted_content'];
codexRequest.service_tier = responsesRequest.service_tier || 'default';
// 删除Codex不支持的字段
delete codexRequest.max_output_tokens;
delete codexRequest.max_completion_tokens;
delete codexRequest.temperature;
delete codexRequest.top_p;
delete codexRequest.service_tier;
delete codexRequest.user;
delete codexRequest.reasoning;
// 添加 reasoning 配置
codexRequest.reasoning = {
"effort": "medium",
"summary": "auto"
"effort": responsesRequest.reasoning_effort || responsesRequest.reasoning?.effort || "medium",
"summary": responsesRequest.reasoning?.summary || "auto"
};
@ -155,11 +154,12 @@ export class CodexConverter extends BaseConverter {
store: false,
metadata: data.metadata || {},
reasoning: {
effort: data.reasoning_effort || 'medium',
summary: 'auto'
effort: data.reasoning_effort || data.reasoning?.effort || 'medium',
summary: data.reasoning?.summary || 'auto'
},
parallel_tool_calls: true,
include: ['reasoning.encrypted_content']
include: ['reasoning.encrypted_content'],
service_tier: data.service_tier || 'default'
};
// 处理 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-preview': 'gemini-3-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-2.5-flash-preview': 'gemini-2.5-flash',
'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-high': 'gemini-3-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-2.5-flash': 'gemini-2.5-flash-preview',
'claude-sonnet-4-5': 'gemini-claude-sonnet-4-5',
@ -129,8 +131,8 @@ function isImageModel(modelName) {
function modelSupportsThinking(modelName) {
if (!modelName) return false;
const name = modelName.toLowerCase();
// 支持 thinking 的模型gemini-3-*, gemini-2.5-*, claude-*-thinking
return name.startsWith('gemini-3-') ||
// 支持 thinking 的模型gemini-3*, gemini-2.5-*, claude-*-thinking
return name.startsWith('gemini-3') ||
name.startsWith('gemini-2.5-') ||
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,
});
const GROK_MODELS = getProviderModels(MODEL_PROVIDER.GROK_CUSTOM);
const MODEL_MAPPING = {
const CORE_MODEL_MAPPING = {
'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-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' }
};
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 {
constructor(config) {
this.config = config;
@ -63,11 +80,70 @@ export class GrokApiService {
this.baseUrl = config.GROK_BASE_URL || 'https://grok.com';
this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`;
this.isInitialized = false;
this.nsfwSetupDone = false;
this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK);
if (this.converter && this.uuid) this.converter.setUuid(this.uuid);
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) {
const sidecar = getTLSSidecar();
if (sidecar.isReady()) {
@ -243,7 +319,9 @@ export class GrokApiService {
}
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 toolOverrides = requestBody.toolOverrides || {};
let fileAttachments = requestBody.fileAttachments || [];
@ -296,12 +374,13 @@ export class GrokApiService {
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 isNsfw = isGrokNsfwModel(rawModelId) || requestBody.nsfw === true || requestBody.disableNsfwFilter === true;
return {
"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,
"fileAttachments": fileAttachments, "forceConcise": false, "forceSideBySide": false, "imageAttachments": [], "imageGenerationCount": 2,
"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 });
}
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);
const modelLower = model.toLowerCase();
const isVideoModel = modelLower.includes('video');
const isImageModel = modelLower.includes('imagine') && !isVideoModel && !modelLower.includes('edit');
const isImageEditModel = modelLower.includes('edit');
@ -461,7 +545,8 @@ export class GrokApiService {
requestBody.toolOverrides = { ...requestBody.toolOverrides, videoGen: true };
}
} 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 || [];

View file

@ -10,7 +10,9 @@ import { MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js';
import { getProxyConfigForProvider } from '../../utils/proxy-utils.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';
/**
@ -339,9 +341,19 @@ export class CodexApiService {
// 明确会话维度:优先使用 session_id 或 conversation_id其次 user_id
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 };
delete cleanedBody.metadata;
// 如果是 fast 模型,移除后缀再传给上游
if (isFastModel) {
cleanedBody.model = model.replace('-fast', '');
}
// 生成会话缓存键
// 弱化 model 依赖,以提升同会话跨模型的缓存命中率
// 仅当 sessionId 为 'default' 时加上 model 前缀,提供基础隔离
@ -363,6 +375,11 @@ export class CodexApiService {
// 注意requestBody 已经去除了 metadata
return {
...cleanedBody,
service_tier: cleanedBody.service_tier || defaultServiceTier,
reasoning: {
...cleanedBody.reasoning,
effort: cleanedBody.reasoning?.effort || defaultReasoningEffort
},
stream,
prompt_cache_key: cache.id
};

View file

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