feat: 新增模型支持并扩展功能配置
- 添加 Gemini 3.1 Flash Lite 模型到 provider-models 和别名映射 - 扩展 Codex 转换器以支持 reasoning 配置和服务层级传递 - 为 Codex 模型添加 -fast 后缀变体,自动设置优先级服务层级和高推理强度 - 扩展 Grok 模型以支持 -nsfw 后缀变体,并添加账户级 NSFW 设置流程 - 放宽 Gemini 模型思考功能检测条件以包含所有 gemini-3 系列模型
This commit is contained in:
parent
4a1a382dc2
commit
3f3c9e1a45
6 changed files with 124 additions and 18 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.10.7
|
||||
2.10.8
|
||||
|
|
|
|||
|
|
@ -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 字段(如果存在)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue