diff --git a/.gitignore b/.gitignore index 8a433ab..852f2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ *.log .env .DS_Store +*.txt diff --git a/README.md b/README.md index 5519adf..5671e0f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,14 @@ # droid2api -OpenAI 兼容 API 代理服务器,用于在不同 LLM API 格式之间进行转换。 +OpenAI 兼容的 API 代理服务器,统一访问不同的 LLM 模型。 ## 功能特性 -- **三种接口模式**: - - **统一格式接口**:`/v1/chat/completions` - 支持所有端点类型,自动格式转换 - - **OpenAI 透明代理**:`/v1/responses` - 直接转发 OpenAI 请求,零转换 - - **Anthropic 透明代理**:`/v1/messages` - 直接转发 Anthropic 请求,零转换 -- **标准 OpenAI API 接口**:提供完全兼容 OpenAI 的 API 端点 -- **多格式支持**:支持 Anthropic 和自定义 OpenAI 格式之间的自动转换 -- **流式响应**:自动转换 SSE (Server-Sent Events) 流式响应为标准 OpenAI 格式 -- **自动刷新 API Key**:集成 WorkOS 认证,自动管理和刷新访问令牌(8小时有效期,每6小时自动刷新) -- **智能 Header 管理**:自动添加和管理所有必需的 Factory 特定 headers -- **配置化路由**:通过 config.json 灵活配置模型和端点映射 -- **开发模式**:详细的日志输出,便于调试 +- 🎯 **标准 OpenAI API 接口** - 使用熟悉的 OpenAI API 格式访问所有模型 +- 🔄 **自动格式转换** - 自动处理不同 LLM 提供商的格式差异 +- 🌊 **流式响应支持** - 支持实时流式输出 +- 🔐 **自动认证管理** - 自动刷新和管理 API 访问令牌 +- ⚙️ **灵活配置** - 通过配置文件自定义模型和端点 ## 安装 @@ -22,25 +16,30 @@ OpenAI 兼容 API 代理服务器,用于在不同 LLM API 格式之间进行 npm install ``` -## 配置 +## 快速开始 -### 1. 配置端点和模型 +### 1. 配置认证 -编辑 `config.json` 文件: +设置环境变量或配置文件: + +```bash +# 方式1:环境变量 +export DROID_REFRESH_KEY="your_refresh_token_here" + +# 方式2:配置文件 ~/.factory/auth.json +{ + "access_token": "your_access_token", + "refresh_token": "your_refresh_token" +} +``` + +### 2. 配置模型(可选) + +编辑 `config.json` 添加或修改模型: ```json { "port": 3000, - "endpoint": [ - { - "name": "openai", - "base_url": "https://app.factory.ai/api/llm/o/v1/responses" - }, - { - "name": "anthropic", - "base_url": "https://app.factory.ai/api/llm/a/v1/messages" - } - ], "models": [ { "name": "Claude Opus 4", @@ -48,38 +47,14 @@ npm install "type": "anthropic" }, { - "name": "GPT-5 Codex", - "id": "gpt-5-codex", + "name": "GPT-5", + "id": "gpt-5-2025-08-07", "type": "openai" } - ], - "dev_mode": false + ] } ``` -### 2. 配置认证(二选一) - -#### 方式一:使用环境变量(推荐用于开发/测试) - -```bash -export DROID_REFRESH_KEY="your_refresh_token_here" -``` - -刷新后的 API key 会保存到工作目录的 `auth.json` 文件。 - -#### 方式二:使用配置文件(推荐用于生产环境) - -确保 `~/.factory/auth.json` 文件存在并包含有效的 tokens: - -```json -{ - "access_token": "your_access_token_here", - "refresh_token": "your_refresh_token_here" -} -``` - -刷新后的 tokens 会自动更新到原文件。 - ## 使用方法 ### 启动服务器 @@ -96,260 +71,52 @@ npm start 服务器默认运行在 `http://localhost:3000`。 -### API 端点总览 +### API 使用 -| 端点 | 方法 | 支持类型 | 格式转换 | 适用场景 | -|------|------|---------|---------|---------| -| `/v1/models` | GET | - | - | 获取模型列表 | -| `/v1/chat/completions` | POST | anthropic, openai | ✅ 自动转换 | 需要统一OpenAI格式 | -| `/v1/responses` | POST | 仅 openai | ❌ 直接转发 | 已是目标格式,追求性能 | -| `/v1/messages` | POST | 仅 anthropic | ❌ 直接转发 | 已是目标格式,追求性能 | +#### 获取模型列表 -### API 端点详细说明 - -#### 1. 获取可用模型列表 - -```bash -GET /v1/models -``` - -**示例:** ```bash curl http://localhost:3000/v1/models ``` -**响应:** -```json -{ - "object": "list", - "data": [ - { - "id": "claude-opus-4-1-20250805", - "object": "model", - "created": 1704067200000, - "owned_by": "factory" - } - ] -} -``` +#### 对话补全 -#### 2. 统一格式接口 - 对话补全(带格式转换) +使用标准 OpenAI 格式调用任何模型: -```bash -POST /v1/chat/completions -``` - -**功能特点:** -- ✅ 支持所有端点类型(anthropic, openai) -- ✅ 自动转换请求格式到目标端点格式 -- ✅ 自动转换响应为标准 OpenAI 格式 -- ✅ 适合需要统一接口的场景 - -**请求参数:** -- `model` (必需): 模型 ID -- `messages` (必需): 标准 OpenAI 格式消息数组 -- `stream` (可选): 是否使用流式响应,默认 true -- `max_tokens` (可选): 最大输出 tokens 数 -- `temperature` (可选): 温度参数 0-1 -- `top_p` (可选): Top-p 采样参数 - -**示例(Anthropic 模型,自动转换):** ```bash curl http://localhost:3000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-4-1-20250805", "messages": [ - {"role": "user", "content": "你好,请介绍一下你自己"} + {"role": "user", "content": "你好"} ], - "stream": true, - "max_tokens": 2000 - }' -``` - -**示例(OpenAI 模型,自动转换):** -```bash -curl http://localhost:3000/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "gpt-5-codex", - "messages": [ - {"role": "user", "content": "写一个 Python 快速排序"} - ], - "stream": false - }' -``` - -#### 3. OpenAI 透明代理接口(不做转换) - -```bash -POST /v1/responses -``` - -**功能特点:** -- ⚠️ **仅支持 openai 类型端点** -- ❌ 请求体不做任何转换,直接转发 -- ❌ 响应体不做任何转换,直接转发 -- ✅ 适合已是目标格式,追求最高性能的场景 - -**限制:** -使用非 openai 类型模型会返回 400 错误: -```json -{ - "error": "Invalid endpoint type", - "message": "/v1/responses 接口只支持 openai 类型端点" -} -``` - -**示例:** -```bash -curl http://localhost:3000/v1/responses \ - -H "Content-Type: application/json" \ - -d '{ - "model": "gpt-5-codex", - "messages": [{"role": "user", "content": "Hello"}], "stream": true }' ``` -#### 4. Anthropic 透明代理接口(不做转换) +**支持的参数:** +- `model` - 模型 ID(必需) +- `messages` - 对话消息数组(必需) +- `stream` - 是否流式输出(默认 true) +- `max_tokens` - 最大输出长度 +- `temperature` - 温度参数(0-1) -```bash -POST /v1/messages -``` +## 常见问题 -**功能特点:** -- ⚠️ **仅支持 anthropic 类型端点** -- ❌ 请求体不做任何转换,直接转发 -- ❌ 响应体不做任何转换,直接转发 -- ✅ 适合已是目标格式,追求最高性能的场景 +### 如何更改端口? + +编辑 `config.json` 中的 `port` 字段: -**限制:** -使用非 anthropic 类型模型会返回 400 错误: ```json { - "error": "Invalid endpoint type", - "message": "/v1/messages 接口只支持 anthropic 类型端点" + "port": 8080 } ``` -**示例:** -```bash -curl http://localhost:3000/v1/messages \ - -H "Content-Type: application/json" \ - -d '{ - "model": "claude-opus-4-1-20250805", - "messages": [{"role": "user", "content": "Hello"}], - "max_tokens": 1024, - "stream": true - }' -``` +### 如何启用调试日志? -## API Key 自动刷新机制 - -代理服务器会自动管理 API key 的刷新: - -1. **启动时刷新**:服务器启动时自动获取新的 access token -2. **定期刷新**:每次 API 请求前检查,如果距离上次刷新超过 6 小时则自动刷新 -3. **令牌有效期**:access token 有效期为 8 小时 -4. **自动保存**:刷新后的 tokens 自动保存到相应的配置文件 - -**刷新日志示例:** -``` -[INFO] Refreshing API key... -[INFO] Authenticated as: user@example.com (John Doe) -[INFO] User ID: user_01K69S755R2TWYFWKPSP74TRKZ -[INFO] Organization ID: org_01K69S7KKYK6F2WYJ8CB384GW6 -[INFO] API key refreshed successfully -``` - -## 接口模式选择指南 - -### 何时使用 `/v1/chat/completions`(统一格式) - -✅ **推荐场景:** -- 需要统一的 OpenAI 兼容接口 -- 应用代码已使用 OpenAI SDK -- 需要在不同 LLM 提供商之间切换 -- 不关心轻微的性能损耗 - -❌ **不推荐场景:** -- 已有原生格式的请求/响应处理逻辑 -- 对性能要求极高(需要避免格式转换开销) - -### 何时使用 `/v1/responses`(OpenAI 透明代理) - -✅ **推荐场景:** -- 请求已经是目标 OpenAI 端点格式 -- 追求最高性能,避免格式转换开销 -- 只使用 OpenAI 端点 - -❌ **不推荐场景:** -- 使用 Anthropic 端点(会返回错误) -- 需要格式转换 - -### 何时使用 `/v1/messages`(Anthropic 透明代理) - -✅ **推荐场景:** -- 请求已经是标准 Anthropic 格式 -- 追求最高性能,避免格式转换开销 -- 只使用 Anthropic 端点 - -❌ **不推荐场景:** -- 使用 OpenAI 端点(会返回错误) -- 需要格式转换 - -## 格式转换说明 - -> 注意:仅 `/v1/chat/completions` 接口会进行格式转换,`/v1/responses` 和 `/v1/messages` 直接转发,不做任何转换。 - -### Anthropic 格式转换(仅 /v1/chat/completions) - -**请求转换:** -- `messages` → `messages`(提取 system 消息到顶层) -- `max_tokens` → `max_tokens`(默认 4096) -- 文本内容包装为 `{type: 'text', text: '...'}` -- 工具格式转换 - -**响应转换:** -- 转换 SSE 事件:`message_start`, `content_block_delta`, `message_delta`, `message_stop` -- 转换为标准 OpenAI chunk 格式 -- 映射停止原因:`end_turn` → `stop`, `max_tokens` → `length` - -### OpenAI 格式转换(仅 /v1/chat/completions) - -**请求转换:** -- `messages` → `input` -- `max_tokens` → `max_output_tokens` -- 用户消息:`text` → `input_text` -- 助手消息:`text` → `output_text` -- 提取 system 消息为 `instructions` 参数 - -**响应转换:** -- 转换 SSE 事件:`response.created`, `response.in_progress`, `response.done` -- 转换为标准 OpenAI chunk 格式 - -## Header 管理 - -代理服务器会自动添加所有必需的 headers: - -### Anthropic 端点 -- `x-model-provider: anthropic` -- `x-factory-client: cli` -- `user-agent: a$/JS 0.57.0` -- `anthropic-version: 2023-06-01` -- `anthropic-beta: interleaved-thinking-2025-05-14` -- `x-stainless-helper-method: stream`(流式请求) -- 自动生成的 UUID:`x-session-id`, `x-assistant-message-id` - -### OpenAI 端点 -- `x-factory-client: cli` -- `user-agent: cB/JS 5.22.0` -- 自动生成的 UUID:`x-session-id`, `x-assistant-message-id` - -## 开发模式 - -在 `config.json` 中设置 `dev_mode: true` 可以启用详细日志: +在 `config.json` 中设置: ```json { @@ -357,58 +124,17 @@ curl http://localhost:3000/v1/messages \ } ``` -**日志内容包括:** -- 完整的请求和响应 headers -- 请求体和响应体 -- 格式转换过程 -- SSE 事件处理详情 - -## 端口冲突处理 - -如果端口 3000 已被占用,可以: - -1. **修改配置文件**:编辑 `config.json` 中的 `port` 字段 -2. **或者结束占用进程**: - ```bash - lsof -ti:3000 | xargs kill -9 - ``` - ## 故障排查 -### 启动时报错 "Refresh token not found" +### 认证失败 -**原因**:未配置 refresh token - -**解决方案**: +确保已正确配置 refresh token: - 设置环境变量 `DROID_REFRESH_KEY` -- 或配置 `~/.factory/auth.json` 文件 +- 或创建 `~/.factory/auth.json` 文件 -### 请求返回 401 错误 +### 模型不可用 -**可能原因**: -1. refresh token 已过期或无效 -2. API key 刷新失败 - -**解决方案**: -- 检查日志中的刷新错误信息 -- 重新获取有效的 refresh token -- 确认 `~/.factory/auth.json` 中的 tokens 正确 - -### 响应格式错误 - -**原因**:模型类型配置错误 - -**解决方案**: -- 检查 `config.json` 中模型的 `type` 字段 -- Anthropic 模型使用 `"type": "anthropic"` -- OpenAI 模型使用 `"type": "openai"` - -## 技术架构 - -- **语言**:Node.js (ES Modules) -- **框架**:Express -- **HTTP 客户端**:node-fetch -- **认证**:WorkOS OAuth 2.0 Refresh Token Flow +检查 `config.json` 中的模型配置,确保模型 ID 和类型正确。 ## 许可证 diff --git a/config.js b/config.js index d1a7119..9ecbc2b 100644 --- a/config.js +++ b/config.js @@ -44,3 +44,8 @@ export function getPort() { const cfg = getConfig(); return cfg.port || 3000; } + +export function getSystemPrompt() { + const cfg = getConfig(); + return cfg.system_prompt || ''; +} diff --git a/config.json b/config.json index 24ad81b..2426be8 100644 --- a/config.json +++ b/config.json @@ -8,6 +8,10 @@ { "name": "anthropic", "base_url": "https://app.factory.ai/api/llm/a/v1/messages" + }, + { + "name": "common", + "base_url": "https://app.factory.ai/api/llm/o/v1/chat/completions" } ], "models": [ @@ -35,7 +39,13 @@ "name": "GPT-5-Codex", "id": "gpt-5-codex", "type": "openai" + }, + { + "name": "GLM-4.6", + "id": "glm-4.6", + "type": "common" } ], - "dev_mode": false + "dev_mode": false, + "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n" } diff --git a/package.json b/package.json index 46707a7..f3fd63c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "droid2api", - "version": "1.0.0", + "version": "1.1.0", "description": "OpenAI Compatible API Proxy", "main": "server.js", "type": "module", diff --git a/routes.js b/routes.js index 4d0824f..55e4ebe 100644 --- a/routes.js +++ b/routes.js @@ -1,9 +1,10 @@ import express from 'express'; import fetch from 'node-fetch'; -import { getConfig, getModelById, getEndpointByType } from './config.js'; +import { getConfig, getModelById, getEndpointByType, getSystemPrompt } 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'; +import { transformToCommon, getCommonHeaders } from './transformers/request-common.js'; import { AnthropicResponseTransformer } from './transformers/response-anthropic.js'; import { OpenAIResponseTransformer } from './transformers/response-openai.js'; import { getApiKey } from './auth.js'; @@ -93,6 +94,9 @@ async function handleChatCompletions(req, res) { } else if (model.type === 'openai') { transformedRequest = transformToOpenAI(openaiRequest); headers = getOpenAIHeaders(authHeader, clientHeaders); + } else if (model.type === 'common') { + transformedRequest = transformToCommon(openaiRequest); + headers = getCommonHeaders(authHeader, clientHeaders); } else { return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` }); } @@ -123,22 +127,37 @@ async function handleChatCompletions(req, res) { res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); - let transformer; - if (model.type === 'anthropic') { - transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`); - } else if (model.type === 'openai') { - transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`); - } - - try { - for await (const chunk of transformer.transformStream(response.body)) { - res.write(chunk); + // common 类型直接转发,不使用 transformer + if (model.type === 'common') { + try { + for await (const chunk of response.body) { + res.write(chunk); + } + res.end(); + logInfo('Stream forwarded (common type)'); + } catch (streamError) { + logError('Stream error', streamError); + res.end(); + } + } else { + // anthropic 和 openai 类型使用 transformer + let transformer; + if (model.type === 'anthropic') { + transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`); + } else if (model.type === 'openai') { + transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`); + } + + try { + for await (const chunk of transformer.transformStream(response.body)) { + res.write(chunk); + } + res.end(); + logInfo('Stream completed'); + } catch (streamError) { + logError('Stream error', streamError); + res.end(); } - res.end(); - logInfo('Stream completed'); - } catch (streamError) { - logError('Stream error', streamError); - res.end(); } } else { const data = await response.json(); @@ -201,16 +220,29 @@ async function handleDirectResponses(req, res) { const clientHeaders = req.headers; - // 获取 headers,但请求体不做任何转换 + // 获取 headers const headers = getOpenAIHeaders(authHeader, clientHeaders); - logRequest('POST', endpoint.base_url, headers, openaiRequest); + // 注入系统提示到 instructions 字段 + const systemPrompt = getSystemPrompt(); + const modifiedRequest = { ...openaiRequest }; + if (systemPrompt) { + // 如果已有 instructions,则在前面添加系统提示 + if (modifiedRequest.instructions) { + modifiedRequest.instructions = systemPrompt + modifiedRequest.instructions; + } else { + // 否则直接设置系统提示 + modifiedRequest.instructions = systemPrompt; + } + } - // 直接转发原始请求 + logRequest('POST', endpoint.base_url, headers, modifiedRequest); + + // 转发修改后的请求 const response = await fetch(endpoint.base_url, { method: 'POST', headers, - body: JSON.stringify(openaiRequest) // 不做任何转换,直接转发 + body: JSON.stringify(modifiedRequest) }); logInfo(`Response status: ${response.status}`); @@ -305,17 +337,35 @@ async function handleDirectMessages(req, res) { const clientHeaders = req.headers; - // 获取 headers,但请求体不做任何转换 + // 获取 headers const isStreaming = anthropicRequest.stream !== false; const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming); - logRequest('POST', endpoint.base_url, headers, anthropicRequest); + // 注入系统提示到 system 字段 + const systemPrompt = getSystemPrompt(); + const modifiedRequest = { ...anthropicRequest }; + if (systemPrompt) { + if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) { + // 如果已有 system 数组,则在最前面插入系统提示 + modifiedRequest.system = [ + { type: 'text', text: systemPrompt }, + ...modifiedRequest.system + ]; + } else { + // 否则创建新的 system 数组 + modifiedRequest.system = [ + { type: 'text', text: systemPrompt } + ]; + } + } - // 直接转发原始请求 + logRequest('POST', endpoint.base_url, headers, modifiedRequest); + + // 转发修改后的请求 const response = await fetch(endpoint.base_url, { method: 'POST', headers, - body: JSON.stringify(anthropicRequest) // 不做任何转换,直接转发 + body: JSON.stringify(modifiedRequest) }); logInfo(`Response status: ${response.status}`); diff --git a/transformers/request-anthropic.js b/transformers/request-anthropic.js index e2b863c..ef30393 100644 --- a/transformers/request-anthropic.js +++ b/transformers/request-anthropic.js @@ -1,4 +1,5 @@ import { logDebug } from '../logger.js'; +import { getSystemPrompt } from '../config.js'; export function transformToAnthropic(openaiRequest) { logDebug('Transforming OpenAI request to Anthropic format'); @@ -77,9 +78,19 @@ export function transformToAnthropic(openaiRequest) { } } - // Add system parameter if system content exists - if (systemContent.length > 0) { - anthropicRequest.system = systemContent; + // Add system parameter with system prompt prepended + const systemPrompt = getSystemPrompt(); + if (systemPrompt || systemContent.length > 0) { + anthropicRequest.system = []; + // Prepend system prompt as first element if it exists + if (systemPrompt) { + anthropicRequest.system.push({ + type: 'text', + text: systemPrompt + }); + } + // Add user-provided system content + anthropicRequest.system.push(...systemContent); } // Transform tools if present @@ -125,11 +136,11 @@ export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming 'anthropic-beta': 'interleaved-thinking-2025-05-14', 'x-api-key': 'placeholder', 'authorization': authHeader || '', - 'x-model-provider': 'anthropic', + 'x-api-provider': 'anthropic', 'x-factory-client': 'cli', 'x-session-id': sessionId, 'x-assistant-message-id': messageId, - 'user-agent': 'a$/JS 0.57.0', + 'user-agent': 'uX/JS 0.57.0', 'x-stainless-timeout': '600', 'connection': 'keep-alive' }; diff --git a/transformers/request-common.js b/transformers/request-common.js new file mode 100644 index 0000000..8424d05 --- /dev/null +++ b/transformers/request-common.js @@ -0,0 +1,88 @@ +import { logDebug } from '../logger.js'; +import { getSystemPrompt } from '../config.js'; + +export function transformToCommon(openaiRequest) { + logDebug('Transforming OpenAI request to Common format'); + + // 基本保持 OpenAI 格式,只在 messages 前面插入 system 消息 + const commonRequest = { + ...openaiRequest + }; + + const systemPrompt = getSystemPrompt(); + + if (systemPrompt) { + // 检查是否已有 system 消息 + const hasSystemMessage = commonRequest.messages?.some(m => m.role === 'system'); + + if (hasSystemMessage) { + // 如果已有 system 消息,在第一个 system 消息前插入我们的 system prompt + commonRequest.messages = commonRequest.messages.map((msg, index) => { + if (msg.role === 'system' && index === commonRequest.messages.findIndex(m => m.role === 'system')) { + // 找到第一个 system 消息,前置我们的 prompt + return { + role: 'system', + content: systemPrompt + (typeof msg.content === 'string' ? msg.content : '') + }; + } + return msg; + }); + } else { + // 如果没有 system 消息,在 messages 数组最前面插入 + commonRequest.messages = [ + { + role: 'system', + content: systemPrompt + }, + ...(commonRequest.messages || []) + ]; + } + } + + logDebug('Transformed Common request', commonRequest); + return commonRequest; +} + +export function getCommonHeaders(authHeader, clientHeaders = {}) { + // Generate unique IDs if not provided + const sessionId = clientHeaders['x-session-id'] || generateUUID(); + const messageId = clientHeaders['x-assistant-message-id'] || generateUUID(); + + const headers = { + 'accept': 'application/json', + 'content-type': 'application/json', + 'authorization': authHeader || '', + 'x-api-provider': 'baseten', + 'x-factory-client': 'cli', + 'x-session-id': sessionId, + 'x-assistant-message-id': messageId, + 'user-agent': 'pB/JS 5.23.2', + 'connection': 'keep-alive' + }; + + // Pass through Stainless SDK headers with defaults + const stainlessDefaults = { + 'x-stainless-arch': 'x64', + 'x-stainless-lang': 'js', + 'x-stainless-os': 'MacOS', + 'x-stainless-runtime': 'node', + 'x-stainless-retry-count': '0', + 'x-stainless-package-version': '5.23.2', + 'x-stainless-runtime-version': 'v24.3.0' + }; + + // Copy Stainless headers from client or use defaults + Object.keys(stainlessDefaults).forEach(header => { + headers[header] = clientHeaders[header] || stainlessDefaults[header]; + }); + + return headers; +} + +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} diff --git a/transformers/request-openai.js b/transformers/request-openai.js index 538d9be..d9c8bd2 100644 --- a/transformers/request-openai.js +++ b/transformers/request-openai.js @@ -1,4 +1,5 @@ import { logDebug } from '../logger.js'; +import { getSystemPrompt } from '../config.js'; export function transformToOpenAI(openaiRequest) { logDebug('Transforming OpenAI request to target OpenAI format'); @@ -66,18 +67,25 @@ export function transformToOpenAI(openaiRequest) { })); } - // Extract system message as instructions + // Extract system message as instructions and prepend system prompt + const systemPrompt = getSystemPrompt(); const systemMessage = openaiRequest.messages?.find(m => m.role === 'system'); + if (systemMessage) { + let userInstructions = ''; if (typeof systemMessage.content === 'string') { - targetRequest.instructions = systemMessage.content; + userInstructions = systemMessage.content; } else if (Array.isArray(systemMessage.content)) { - targetRequest.instructions = systemMessage.content + userInstructions = systemMessage.content .filter(p => p.type === 'text') .map(p => p.text) .join('\n'); } + targetRequest.instructions = systemPrompt + userInstructions; targetRequest.input = targetRequest.input.filter(m => m.role !== 'system'); + } else if (systemPrompt) { + // If no user-provided system message, just add the system prompt + targetRequest.instructions = systemPrompt; } // Pass through other parameters @@ -109,11 +117,11 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) { const headers = { 'content-type': 'application/json', 'authorization': authHeader || '', - 'x-api-key': 'placeholder', + 'x-api-provider': 'azure_openai', 'x-factory-client': 'cli', 'x-session-id': sessionId, 'x-assistant-message-id': messageId, - 'user-agent': 'cB/JS 5.22.0', + 'user-agent': 'pB/JS 5.23.2', 'connection': 'keep-alive' }; @@ -124,7 +132,7 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) { 'x-stainless-os': 'MacOS', 'x-stainless-runtime': 'node', 'x-stainless-retry-count': '0', - 'x-stainless-package-version': '5.22.0', + 'x-stainless-package-version': '5.23.2', 'x-stainless-runtime-version': 'v24.3.0' }; @@ -133,8 +141,6 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) { headers[header] = clientHeaders[header] || stainlessDefaults[header]; }); - - return headers; }