Compare commits

...

10 Commits

Author SHA1 Message Date
1eon
a8928bce32 update model 2025-12-24 03:50:26 +08:00
1eon
3626c6683a Merge branch 'main' of https://github.com/1e0n/droid2api 2025-12-24 03:48:17 +08:00
1eon
5e962cb00f feat: Remove gpt-5 redirect and Opus 4.1 model, and add GPT-5.2 and Gemini-3-Flash models. 2025-12-24 03:44:04 +08:00
1e0n
60754b65cf add gpt 5.2 2025-12-12 04:22:20 +08:00
1eon
aa3bb3c65b add gpt-5.1-codex-max support 2025-12-05 21:11:53 +08:00
1eon
93284c80ff update models 2025-11-25 12:24:33 +08:00
1eon
0f4f2e3509 add gemini 3.0 pro support 2025-11-19 01:29:45 +08:00
1eon
62a384f34b feat: add dynamic x-api-provider and reasoning_effort support
- Add per-model provider configuration in config.json
- Implement getModelProvider() to fetch provider from model config
- Update all header generators to accept dynamic provider parameter
- Add reasoning_effort field handling for common endpoint type
- Support auto/low/medium/high/off reasoning levels for common models

This enables flexible multi-provider support and reasoning control
across different endpoint types (anthropic, openai, common).
2025-11-19 01:25:01 +08:00
1eon
c31b680d95 feat: add dynamic user-agent version updater
- Add user-agent-updater.js to automatically fetch latest factory-cli version
- Fetch version from https://downloads.factory.ai/factory-cli/LATEST on startup
- Automatically refresh version every hour
- Implement retry mechanism: max 3 retries with 1-minute intervals on failure
- Use user_agent from config.json as fallback value
- Update config.js to use dynamic user-agent
- Initialize updater in server.js startup sequence
2025-11-16 16:25:15 +08:00
1e0n
3c0e922cbd add gpt-5.1 and gpt-5.1-codex support 2025-11-14 11:33:37 +08:00
10 changed files with 231 additions and 41 deletions

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { logInfo } from './logger.js';
import { getCurrentUserAgent } from './user-agent-updater.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -57,15 +58,19 @@ export function getModelReasoning(modelId) {
return null;
}
const reasoningLevel = model.reasoning.toLowerCase();
if (['low', 'medium', 'high', 'auto'].includes(reasoningLevel)) {
if (['low', 'medium', 'high', 'xhigh', 'auto'].includes(reasoningLevel)) {
return reasoningLevel;
}
return null;
}
export function getModelProvider(modelId) {
const model = getModelById(modelId);
return model?.provider || null;
}
export function getUserAgent() {
const cfg = getConfig();
return cfg.user_agent || 'factory-cli/0.19.3';
return getCurrentUserAgent();
}
export function getProxyConfigs() {

View File

@@ -2,8 +2,7 @@
"port": 3000,
"model_redirects": {
"claude-3-5-haiku-20241022": "claude-haiku-4-5-20251001",
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
"gpt-5":"gpt-5-2025-08-07"
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929"
},
"endpoint": [
{
@@ -22,42 +21,77 @@
"proxies": [],
"models": [
{
"name": "Opus 4.1",
"id": "claude-opus-4-1-20250805",
"name": "Opus 4.5",
"id": "claude-opus-4-5-20251101",
"type": "anthropic",
"reasoning": "auto"
"reasoning": "auto",
"provider": "anthropic"
},
{
"name": "Haiku 4.5",
"id": "claude-haiku-4-5-20251001",
"type": "anthropic",
"reasoning": "auto"
"reasoning": "auto",
"provider": "anthropic"
},
{
"name": "Sonnet 4.5",
"id": "claude-sonnet-4-5-20250929",
"type": "anthropic",
"reasoning": "auto"
"reasoning": "auto",
"provider": "anthropic"
},
{
"name": "GPT-5",
"id": "gpt-5-2025-08-07",
"name": "GPT-5.2",
"id": "gpt-5.2",
"type": "openai",
"reasoning": "auto"
"reasoning": "auto",
"provider": "openai"
},
{
"name": "GPT-5-Codex",
"id": "gpt-5-codex",
"name": "GPT-5.1",
"id": "gpt-5.1",
"type": "openai",
"reasoning": "off"
"reasoning": "auto",
"provider": "openai"
},
{
"name": "GPT-5.1 Codex",
"id": "gpt-5.1-codex",
"type": "openai",
"reasoning": "off",
"provider": "openai"
},
{
"name": "GPT-5.1 Codex Max",
"id": "gpt-5.1-codex-max",
"type": "openai",
"reasoning": "auto",
"provider": "openai"
},
{
"name": "GLM-4.6",
"id": "glm-4.6",
"type": "common"
"type": "common",
"reasoning": "off",
"provider": "fireworks"
},
{
"name": "Gemini-3-Pro",
"id": "gemini-3-pro-preview",
"type": "common",
"reasoning": "auto",
"provider": "google"
},
{
"name": "Gemini-3-Flash",
"id": "gemini-3-flash-preview",
"type": "common",
"reasoning": "auto",
"provider": "google"
}
],
"dev_mode": false,
"user_agent": "factory-cli/0.25.1",
"user_agent": "factory-cli/0.27.1",
"system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "droid2api",
"version": "1.3.6",
"version": "1.3.7",
"description": "OpenAI Compatible API Proxy",
"main": "server.js",
"type": "module",

View File

@@ -1,6 +1,6 @@
import express from 'express';
import fetch from 'node-fetch';
import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning, getRedirectedModelId } from './config.js';
import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning, getRedirectedModelId, getModelProvider } from './config.js';
import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js';
import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js';
import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js';
@@ -129,16 +129,19 @@ async function handleChatCompletions(req, res) {
// Update request body with redirected model ID before transformation
const requestWithRedirectedModel = { ...openaiRequest, model: modelId };
// Get provider from model config
const provider = getModelProvider(modelId);
if (model.type === 'anthropic') {
transformedRequest = transformToAnthropic(requestWithRedirectedModel);
const isStreaming = openaiRequest.stream === true;
headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId);
headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider);
} else if (model.type === 'openai') {
transformedRequest = transformToOpenAI(requestWithRedirectedModel);
headers = getOpenAIHeaders(authHeader, clientHeaders);
headers = getOpenAIHeaders(authHeader, clientHeaders, provider);
} else if (model.type === 'common') {
transformedRequest = transformToCommon(requestWithRedirectedModel);
headers = getCommonHeaders(authHeader, clientHeaders);
headers = getCommonHeaders(authHeader, clientHeaders, provider);
} else {
return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` });
}
@@ -285,8 +288,11 @@ async function handleDirectResponses(req, res) {
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
// 获取 headers
const headers = getOpenAIHeaders(authHeader, clientHeaders);
const headers = getOpenAIHeaders(authHeader, clientHeaders, provider);
// 注入系统提示到 instructions 字段并更新重定向后的模型ID
const systemPrompt = getSystemPrompt();
@@ -306,7 +312,7 @@ async function handleDirectResponses(req, res) {
if (reasoningLevel === 'auto') {
// Auto模式保持原始请求的reasoning字段不变
// 如果原始请求有reasoning字段就保留没有就不添加
} else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
modifiedRequest.reasoning = {
effort: reasoningLevel,
summary: 'auto'
@@ -427,9 +433,12 @@ async function handleDirectMessages(req, res) {
const clientHeaders = req.headers;
// Get provider from model config
const provider = getModelProvider(modelId);
// 获取 headers
const isStreaming = anthropicRequest.stream === true;
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId);
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider);
// 注入系统提示到 system 字段并更新重定向后的模型ID
const systemPrompt = getSystemPrompt();
@@ -454,11 +463,12 @@ async function handleDirectMessages(req, res) {
if (reasoningLevel === 'auto') {
// Auto模式保持原始请求的thinking字段不变
// 如果原始请求有thinking字段就保留没有就不添加
} else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
const budgetTokens = {
'low': 4096,
'medium': 12288,
'high': 24576
'high': 24576,
'xhigh': 40960
};
modifiedRequest.thinking = {
@@ -576,7 +586,11 @@ async function handleCountTokens(req, res) {
}
const clientHeaders = req.headers;
const headers = getAnthropicHeaders(authHeader, clientHeaders, false, modelId);
// Get provider from model config
const provider = getModelProvider(modelId);
const headers = getAnthropicHeaders(authHeader, clientHeaders, false, modelId, provider);
// 构建 count_tokens 端点 URL
const countTokensUrl = endpoint.base_url.replace('/v1/messages', '/v1/messages/count_tokens');

View File

@@ -3,6 +3,7 @@ import { loadConfig, isDevMode, getPort } from './config.js';
import { logInfo, logError } from './logger.js';
import router from './routes.js';
import { initializeAuth } from './auth.js';
import { initializeUserAgentUpdater } from './user-agent-updater.js';
const app = express();
@@ -112,6 +113,9 @@ app.use((err, req, res, next) => {
logInfo('Configuration loaded successfully');
logInfo(`Dev mode: ${isDevMode()}`);
// Initialize User-Agent version updater
initializeUserAgentUpdater();
// Initialize auth system (load and setup API key if needed)
// This won't throw error if no auth config is found - will use client auth
await initializeAuth();

View File

@@ -1,4 +1,8 @@
#!/bin/bash
echo "FACTORY_API_KEY 当前值是" $FACTORY_API_KEY
echo $FACTORY_API_KEY
echo "Reset FACTORY_API_KEY..."
export FACTORY_API_KEY=""
echo "Starting droid2api server..."
node server.js

View File

@@ -119,12 +119,13 @@ export function transformToAnthropic(openaiRequest) {
anthropicRequest.thinking = openaiRequest.thinking;
}
// If original request has no thinking field, don't add one
} else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
// Specific level: override with model configuration
const budgetTokens = {
'low': 4096,
'medium': 12288,
'high': 24576
'high': 24576,
'xhigh': 24576
};
anthropicRequest.thinking = {
@@ -154,7 +155,7 @@ export function transformToAnthropic(openaiRequest) {
return anthropicRequest;
}
export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming = true, modelId = null) {
export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming = true, modelId = null, provider = 'anthropic') {
// Generate unique IDs if not provided
const sessionId = clientHeaders['x-session-id'] || generateUUID();
const messageId = clientHeaders['x-assistant-message-id'] || generateUUID();
@@ -165,7 +166,7 @@ export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming
'anthropic-version': clientHeaders['anthropic-version'] || '2023-06-01',
'authorization': authHeader || '',
'x-api-key': 'placeholder',
'x-api-provider': 'anthropic',
'x-api-provider': provider,
'x-factory-client': 'cli',
'x-session-id': sessionId,
'x-assistant-message-id': messageId,
@@ -189,7 +190,7 @@ export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming
if (reasoningLevel === 'auto') {
// Auto mode: don't modify anthropic-beta header, preserve original
// betaValues remain unchanged from client headers
} else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
// Add thinking beta if not already present
if (!betaValues.includes(thinkingBeta)) {
betaValues.push(thinkingBeta);

View File

@@ -1,5 +1,5 @@
import { logDebug } from '../logger.js';
import { getSystemPrompt, getUserAgent } from '../config.js';
import { getSystemPrompt, getUserAgent, getModelReasoning } from '../config.js';
export function transformToCommon(openaiRequest) {
logDebug('Transforming OpenAI request to Common format');
@@ -39,11 +39,25 @@ export function transformToCommon(openaiRequest) {
}
}
// Handle reasoning_effort field based on model configuration
const reasoningLevel = getModelReasoning(openaiRequest.model);
if (reasoningLevel === 'auto') {
// Auto mode: preserve original request's reasoning_effort field exactly as-is
// If original request has reasoning_effort field, keep it; otherwise don't add one
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
// Specific level: override with model configuration
commonRequest.reasoning_effort = reasoningLevel;
} else {
// Off or invalid: explicitly remove reasoning_effort field
// This ensures any reasoning_effort field from the original request is deleted
delete commonRequest.reasoning_effort;
}
logDebug('Transformed Common request', commonRequest);
return commonRequest;
}
export function getCommonHeaders(authHeader, clientHeaders = {}) {
export function getCommonHeaders(authHeader, clientHeaders = {}, provider = 'baseten') {
// Generate unique IDs if not provided
const sessionId = clientHeaders['x-session-id'] || generateUUID();
const messageId = clientHeaders['x-assistant-message-id'] || generateUUID();
@@ -52,7 +66,7 @@ export function getCommonHeaders(authHeader, clientHeaders = {}) {
'accept': 'application/json',
'content-type': 'application/json',
'authorization': authHeader || '',
'x-api-provider': 'baseten',
'x-api-provider': provider,
'x-factory-client': 'cli',
'x-session-id': sessionId,
'x-assistant-message-id': messageId,

View File

@@ -100,7 +100,7 @@ export function transformToOpenAI(openaiRequest) {
targetRequest.reasoning = openaiRequest.reasoning;
}
// If original request has no reasoning field, don't add one
} else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {
} else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) {
// Specific level: override with model configuration
targetRequest.reasoning = {
effort: reasoningLevel,
@@ -133,7 +133,7 @@ export function transformToOpenAI(openaiRequest) {
return targetRequest;
}
export function getOpenAIHeaders(authHeader, clientHeaders = {}) {
export function getOpenAIHeaders(authHeader, clientHeaders = {}, provider = 'openai') {
// Generate unique IDs if not provided
const sessionId = clientHeaders['x-session-id'] || generateUUID();
const messageId = clientHeaders['x-assistant-message-id'] || generateUUID();
@@ -141,7 +141,7 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) {
const headers = {
'content-type': 'application/json',
'authorization': authHeader || '',
'x-api-provider': 'azure_openai',
'x-api-provider': provider,
'x-factory-client': 'cli',
'x-session-id': sessionId,
'x-assistant-message-id': messageId,

114
user-agent-updater.js Normal file
View File

@@ -0,0 +1,114 @@
import https from 'https';
import { logInfo, logError } from './logger.js';
import { getConfig } from './config.js';
const VERSION_URL = 'https://downloads.factory.ai/factory-cli/LATEST';
const USER_AGENT_PREFIX = 'factory-cli';
const CHECK_INTERVAL = 60 * 60 * 1000; // 1 hour
const RETRY_INTERVAL = 60 * 1000; // 1 minute
const MAX_RETRIES = 3;
let currentVersion = null;
let isUpdating = false;
function getDefaultVersion() {
const cfg = getConfig();
const userAgent = cfg.user_agent || 'factory-cli/0.19.3';
const match = userAgent.match(/\/(\d+\.\d+\.\d+)/);
return match ? match[1] : '0.19.3';
}
function initializeVersion() {
if (currentVersion === null) {
currentVersion = getDefaultVersion();
}
}
export function getCurrentUserAgent() {
initializeVersion();
return `${USER_AGENT_PREFIX}/${currentVersion}`;
}
function fetchLatestVersion() {
return new Promise((resolve, reject) => {
const request = https.get(VERSION_URL, (res) => {
let data = '';
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}`));
return;
}
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
const version = data.trim();
if (version && /^\d+\.\d+\.\d+/.test(version)) {
resolve(version);
} else {
reject(new Error(`Invalid version format: ${version}`));
}
});
});
request.on('error', (err) => {
reject(err);
});
request.setTimeout(10000, () => {
request.destroy();
reject(new Error('Request timeout'));
});
});
}
async function updateVersionWithRetry(retryCount = 0) {
if (isUpdating) {
return;
}
isUpdating = true;
try {
const version = await fetchLatestVersion();
if (version !== currentVersion) {
const oldVersion = currentVersion;
currentVersion = version;
logInfo(`User-Agent version updated: ${oldVersion} -> ${currentVersion}`);
} else {
logInfo(`User-Agent version is up to date: ${currentVersion}`);
}
isUpdating = false;
} catch (error) {
logError(`Failed to fetch latest version (attempt ${retryCount + 1}/${MAX_RETRIES})`, error);
if (retryCount < MAX_RETRIES - 1) {
logInfo(`Retrying in 1 minute...`);
setTimeout(() => {
updateVersionWithRetry(retryCount + 1);
}, RETRY_INTERVAL);
} else {
logError(`Max retries reached. Will try again in next hourly check.`);
isUpdating = false;
}
}
}
export function initializeUserAgentUpdater() {
initializeVersion();
logInfo('Initializing User-Agent version updater...');
logInfo(`Default User-Agent from config: ${USER_AGENT_PREFIX}/${currentVersion}`);
// Fetch immediately on startup
updateVersionWithRetry();
// Schedule hourly checks
setInterval(() => {
logInfo('Running scheduled User-Agent version check...');
updateVersionWithRetry();
}, CHECK_INTERVAL);
logInfo(`User-Agent updater initialized. Will check every hour.`);
}