Add Droid (Factory.ai) provider support
This commit adds support for Factory.ai's Droid CLI as a provider, enabling users to use Droid through an OpenAI-compatible API interface. Implementation details: - Created DroidApiService that wraps the droid CLI - Added DroidStrategy for protocol conversion (Claude-compatible) - Supports both streaming and non-streaming responses - No API keys or token management required - uses droid CLI directly Files added: - src/droid/droid-core.js: Core service using CLI wrapper - src/droid/droid-strategy.js: Strategy pattern implementation - src/droid/README.md: Comprehensive documentation - test-droid.js: Test script for validation Files modified: - src/adapter.js: Added DroidApiServiceAdapter - src/common.js: Added DROID constants - src/provider-strategies.js: Registered DroidStrategy - README.md: Updated with Droid provider information
This commit is contained in:
parent
262f119213
commit
3111119f93
8 changed files with 657 additions and 2 deletions
21
README.md
21
README.md
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
## 💡 核心优势
|
||||
|
||||
* ✅ **多模型统一接入**:通过统一的 OpenAI 兼容接口,轻松接入 Gemini、OpenAI、Claude、Kimi K2、GLM-4.5、Qwen Code 等多种主流大模型,并通过启动参数或请求头自由切换。
|
||||
* ✅ **多模型统一接入**:通过统一的 OpenAI 兼容接口,轻松接入 Gemini、OpenAI、Claude、Factory Droid、Kimi K2、GLM-4.5、Qwen Code 等多种主流大模型,并通过启动参数或请求头自由切换。
|
||||
* ✅ **突破官方限制**:利用 Gemini CLI 的 OAuth 授权,有效规避官方免费 API 的速率和配额限制,提升请求额度和使用频率。
|
||||
* ✅ **免费使用 Claude Sonnet 4.5**:在 Kiro API 模式下,支持免费使用 Claude Sonnet 4.5 模型。
|
||||
* ✅ **无缝兼容 OpenAI**:提供与 OpenAI API 完全兼容的接口,使 LobeChat, NextChat 等现有工具链和客户端能零成本接入所有支持模型。
|
||||
|
|
@ -119,6 +119,11 @@
|
|||
* **Qwen Code 支持**:
|
||||
* **授权流程**:首次使用 Qwen Code 时,会自动在浏览器中打开授权页面。完成授权后,`oauth_creds.json` 文件将生成并存储在 `~/.qwen` 目录下。
|
||||
* **模型参数**:请使用官方默认参数 `temperature=0` 和 `top_p=1`。
|
||||
* **Droid (Factory.ai) 支持**:
|
||||
* **使用前提**:使用 Droid 需要[安装 Factory CLI](https://factory.ai/product/cli) 并完成认证登录,以生成 `~/.factory/auth.json` 文件。
|
||||
* **认证流程**:运行 `droid` 命令并按提示登录,OAuth tokens 将自动保存。
|
||||
* **优势**:无需单独的 API key,直接使用 Factory.ai 账号的 Claude 访问权限。
|
||||
* **详细文档**:查看 [Droid Provider README](src/droid/README.md) 了解完整配置说明。
|
||||
* **Kiro API**:
|
||||
* **使用前提**:使用 Kiro API 需要[下载 Kiro 客户端](https://aibook.ren/archives/kiro-install)并完成授权登录,以生成 `kiro-auth-token.json` 文件。
|
||||
* **最佳体验**:推荐配合 Claude Code 使用以获得最佳体验。
|
||||
|
|
@ -132,6 +137,7 @@
|
|||
* `http://localhost:3000/openai-custom` - 使用 OpenAI 自定义供应商处理 Claude 请求。
|
||||
* `http://localhost:3000/gemini-cli-oauth` - 使用 Gemini CLI OAuth 供应商处理 Claude 请求。
|
||||
* `http://localhost:3000/openai-qwen-oauth` - 使用 Qwen OAuth 供应商处理 Claude 请求。
|
||||
* `http://localhost:3000/droid-factory-oauth` - 使用 Factory.ai Droid OAuth 供应商访问 Claude API。
|
||||
|
||||
这些 Path 路由不仅适用于直接 API 调用,也可在 Cline、Kilo 等编程 Agent 中配置 API 端点时使用,实现灵活的模型调用。例如,将 Agent 的 API 端点设置为 `http://localhost:3000/claude-kiro-oauth` 即可调用通过 Kiro OAuth 认证的 Claude 模型。
|
||||
|
||||
|
|
@ -173,6 +179,7 @@
|
|||
* **Gemini**: `~/.gemini/oauth_creds.json`
|
||||
* **Kiro**: `~/.aws/sso/cache/kiro-auth-token.json`
|
||||
* **Qwen**: `~/.qwen/oauth_creds.json`
|
||||
* **Droid (Factory.ai)**: `~/.factory/auth.json`
|
||||
|
||||
其中 `~` 代表用户主目录。如果需要自定义路径,可以通过配置文件或环境变量进行设置。
|
||||
|
||||
|
|
@ -256,7 +263,7 @@ $env:HTTP_PROXY="http://your_proxy_address:port"
|
|||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--model-provider` | string | gemini-cli-oauth | AI 模型提供商,可选值:openai-custom, claude-custom, gemini-cli-oauth, claude-kiro-oauth, openai-qwen-oauth |
|
||||
| `--model-provider` | string | gemini-cli-oauth | AI 模型提供商,可选值:openai-custom, claude-custom, gemini-cli-oauth, claude-kiro-oauth, openai-qwen-oauth, droid-factory-oauth |
|
||||
|
||||
### 🧠 OpenAI 兼容提供商参数
|
||||
|
||||
|
|
@ -293,6 +300,13 @@ $env:HTTP_PROXY="http://your_proxy_address:port"
|
|||
|------|------|--------|------|
|
||||
| `--qwen-oauth-creds-file` | string | null | Qwen OAuth 凭据 JSON 文件路径 (当 `model-provider` 为 `openai-qwen-oauth` 时必需) |
|
||||
|
||||
### 🤖 Droid (Factory.ai) OAuth 认证参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--droid-auth-file` | string | ~/.factory/auth.json | Factory Droid OAuth 凭据文件路径 (当 `model-provider` 为 `droid-factory-oauth` 时可选) |
|
||||
| `--droid-base-url` | string | https://api.anthropic.com | Droid 使用的 Claude API 基础 URL (可选) |
|
||||
|
||||
### 📝 系统提示配置参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|
|
@ -348,6 +362,9 @@ node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-ba
|
|||
# 使用Gemini提供商(凭据文件)
|
||||
node src/api-server.js --model-provider gemini-cli-oauth --gemini-oauth-creds-file /path/to/credentials.json --project-id your-project-id
|
||||
|
||||
# 使用Droid (Factory.ai) 提供商
|
||||
node src/api-server.js --model-provider droid-factory-oauth
|
||||
|
||||
# 配置系统提示
|
||||
node src/api-server.js --system-prompt-file custom-prompt.txt --system-prompt-mode append
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { OpenAIApiService } from './openai/openai-core.js'; // 导入OpenAIApiSe
|
|||
import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiService
|
||||
import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService
|
||||
import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService
|
||||
import { DroidApiService } from './droid/droid-core.js'; // 导入DroidApiService
|
||||
import { MODEL_PROVIDER } from './common.js'; // 导入 MODEL_PROVIDER
|
||||
|
||||
// 定义AI服务适配器接口
|
||||
|
|
@ -241,6 +242,46 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// Droid (Factory.ai) API 服务适配器
|
||||
export class DroidApiServiceAdapter extends ApiServiceAdapter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.droidApiService = new DroidApiService(config);
|
||||
}
|
||||
|
||||
async generateContent(model, requestBody) {
|
||||
if (!this.droidApiService.isInitialized) {
|
||||
console.warn("droidApiService not initialized, attempting to re-initialize...");
|
||||
await this.droidApiService.initialize();
|
||||
}
|
||||
return this.droidApiService.generateContent(model, requestBody);
|
||||
}
|
||||
|
||||
async *generateContentStream(model, requestBody) {
|
||||
if (!this.droidApiService.isInitialized) {
|
||||
console.warn("droidApiService not initialized, attempting to re-initialize...");
|
||||
await this.droidApiService.initialize();
|
||||
}
|
||||
yield* this.droidApiService.generateContentStream(model, requestBody);
|
||||
}
|
||||
|
||||
async listModels() {
|
||||
if (!this.droidApiService.isInitialized) {
|
||||
console.warn("droidApiService not initialized, attempting to re-initialize...");
|
||||
await this.droidApiService.initialize();
|
||||
}
|
||||
return this.droidApiService.listModels();
|
||||
}
|
||||
|
||||
async refreshToken() {
|
||||
if (this.droidApiService.isExpiryDateNear()) {
|
||||
console.log(`[Droid] Token expiry near, refreshing...`);
|
||||
return this.droidApiService.refreshAccessToken();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
// 用于存储服务适配器单例的映射
|
||||
export const serviceInstances = {};
|
||||
|
||||
|
|
@ -266,6 +307,9 @@ export function getServiceAdapter(config) {
|
|||
case MODEL_PROVIDER.QWEN_API:
|
||||
serviceInstances[providerKey] = new QwenApiServiceAdapter(config);
|
||||
break;
|
||||
case MODEL_PROVIDER.DROID_API:
|
||||
serviceInstances[providerKey] = new DroidApiServiceAdapter(config);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported model provider: ${provider}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const MODEL_PROTOCOL_PREFIX = {
|
|||
GEMINI: 'gemini',
|
||||
OPENAI: 'openai',
|
||||
CLAUDE: 'claude',
|
||||
DROID: 'droid', // Droid uses Claude protocol
|
||||
}
|
||||
|
||||
export const MODEL_PROVIDER = {
|
||||
|
|
@ -25,6 +26,7 @@ export const MODEL_PROVIDER = {
|
|||
CLAUDE_CUSTOM: 'claude-custom',
|
||||
KIRO_API: 'claude-kiro-oauth',
|
||||
QWEN_API: 'openai-qwen-oauth',
|
||||
DROID_API: 'droid-factory-oauth',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
227
src/droid/README.md
Normal file
227
src/droid/README.md
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
# Droid (Factory.ai) Provider
|
||||
|
||||
This adapter enables you to use Factory.ai's Droid CLI as an OpenAI-compatible API through AIClient2API.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Uses your existing Droid CLI installation
|
||||
- ✅ No need for API keys or token management
|
||||
- ✅ Full Claude API compatibility
|
||||
- ✅ Supports streaming and non-streaming responses
|
||||
- ✅ Works with any OpenAI-compatible client
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Install Droid CLI**
|
||||
```bash
|
||||
# Install Droid CLI from Factory.ai
|
||||
# Visit: https://factory.ai/product/cli
|
||||
```
|
||||
|
||||
2. **Authenticate with Droid**
|
||||
```bash
|
||||
droid
|
||||
# Follow the prompts to login
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Using Command Line Arguments
|
||||
|
||||
```bash
|
||||
node src/api-server.js \
|
||||
--model-provider droid-factory-oauth \
|
||||
--port 3000 \
|
||||
--api-key your-api-key
|
||||
```
|
||||
|
||||
### Using config.json
|
||||
|
||||
Add to your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"MODEL_PROVIDER": "droid-factory-oauth",
|
||||
"PORT": 3000,
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
```
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
```bash
|
||||
export MODEL_PROVIDER=droid-factory-oauth
|
||||
export PORT=3000
|
||||
export API_KEY=your-api-key
|
||||
node src/api-server.js
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### With OpenAI-Compatible Clients
|
||||
|
||||
Point your OpenAI-compatible client to the proxy:
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
base_url="http://localhost:3000/v1",
|
||||
api_key="your-api-key"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello!"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### With Claude SDK
|
||||
|
||||
```python
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic(
|
||||
base_url="http://localhost:3000",
|
||||
api_key="your-api-key" # Your proxy API key, not Anthropic's
|
||||
)
|
||||
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
max_tokens=1024,
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello, Claude!"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### With curl
|
||||
|
||||
```bash
|
||||
# Claude API format
|
||||
curl http://localhost:3000/v1/messages \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: your-api-key" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d '{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 1024,
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello!"}
|
||||
]
|
||||
}'
|
||||
|
||||
# OpenAI API format
|
||||
curl http://localhost:3000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-d '{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello!"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Supported Models
|
||||
|
||||
The Droid provider supports all Claude models available through Factory.ai:
|
||||
|
||||
- `claude-sonnet-4-5-20250929` (default)
|
||||
- `claude-3-7-sonnet-20250219`
|
||||
- `claude-3-5-sonnet-20241022`
|
||||
- `claude-3-opus-20240229`
|
||||
|
||||
## How It Works
|
||||
|
||||
The proxy converts OpenAI/Claude API requests into `droid exec` commands and streams the results back:
|
||||
|
||||
1. Your app sends a request to the proxy (OpenAI or Claude format)
|
||||
2. The proxy converts messages to a prompt
|
||||
3. Executes `droid exec "your prompt"`
|
||||
4. Streams the response back in the requested format
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Droid CLI is not installed or not in PATH"
|
||||
|
||||
**Problem**: The droid command is not available
|
||||
|
||||
**Solution**:
|
||||
1. Install Droid CLI from https://factory.ai/product/cli
|
||||
2. Make sure `droid` is in your system PATH
|
||||
3. Test with: `droid --version`
|
||||
|
||||
### "Droid command failed"
|
||||
|
||||
**Problem**: Droid CLI returned an error
|
||||
|
||||
**Solution**:
|
||||
1. Make sure you are authenticated: `droid`
|
||||
2. Test droid directly: `droid exec "hello"`
|
||||
3. Check for any error messages from the droid CLI
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
**Problem**: Droid needs authentication
|
||||
|
||||
**Solution**:
|
||||
1. Run `droid` to start an interactive session and authenticate
|
||||
2. Follow the browser authentication flow
|
||||
3. After authentication, the proxy will work automatically
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Your App → AIClient2API Proxy → droid exec command → Factory.ai
|
||||
(Port 3000) (CLI process)
|
||||
```
|
||||
|
||||
The Droid provider:
|
||||
1. Receives API requests (OpenAI or Claude format)
|
||||
2. Converts messages to prompt text
|
||||
3. Spawns `droid exec "prompt"` process
|
||||
4. Streams output back to client
|
||||
5. Converts response to requested API format
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Droid Command
|
||||
|
||||
If your droid CLI is installed with a different name or path:
|
||||
|
||||
```json
|
||||
{
|
||||
"MODEL_PROVIDER": "droid-factory-oauth",
|
||||
"DROID_COMMAND": "/custom/path/to/droid"
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **API key protection**: Use strong API keys for the proxy itself
|
||||
2. **Local execution**: Droid CLI runs locally with your credentials
|
||||
3. **Use HTTPS in production**: Don't expose the proxy over HTTP in production
|
||||
4. **Access control**: Limit who can access your proxy server
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires Droid CLI to be installed and authenticated
|
||||
- Each request spawns a new `droid exec` process
|
||||
- Streaming support depends on droid CLI output behavior
|
||||
- Authentication is managed by Droid CLI (not the proxy)
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing Droid-related changes:
|
||||
|
||||
1. Test with actual Droid CLI installation
|
||||
2. Ensure both streaming and non-streaming work
|
||||
3. Update this README with any new features
|
||||
4. Follow the existing code patterns in the project
|
||||
|
||||
## License
|
||||
|
||||
This provider follows the same license as the main AIClient2API project (GPLv3).
|
||||
229
src/droid/droid-core.js
Normal file
229
src/droid/droid-core.js
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import { spawn } from 'child_process';
|
||||
|
||||
/**
|
||||
* Droid (Factory.ai) API Service
|
||||
* Wraps the Droid CLI to provide an API-compatible interface
|
||||
*/
|
||||
export class DroidApiService {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {object} config - Configuration object
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.config = config;
|
||||
this.isInitialized = false;
|
||||
this.droidCommand = config.DROID_COMMAND || 'droid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the service by checking if droid CLI is available
|
||||
* @returns {Promise<boolean>} True if initialization succeeded
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
console.log('[Droid] Initializing Droid API Service...');
|
||||
await this.executeDroidCommand(['--version']);
|
||||
this.isInitialized = true;
|
||||
console.log('[Droid] Initialization complete.');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Droid] Droid CLI not available:', error.message);
|
||||
this.isInitialized = false;
|
||||
throw new Error('Droid CLI is not installed or not in PATH. Please install from https://factory.ai/product/cli');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute droid CLI command and return output
|
||||
* @param {Array<string>} args - Command arguments
|
||||
* @returns {Promise<string>} Command output
|
||||
*/
|
||||
async executeDroidCommand(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const droid = spawn(this.droidCommand, args);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
droid.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
droid.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
droid.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Droid command failed: ${stderr || stdout}`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
|
||||
droid.on('error', (err) => {
|
||||
reject(new Error(`Failed to spawn droid: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Claude-format messages to a simple prompt string
|
||||
* @param {Array<object>} messages - Array of message objects
|
||||
* @returns {string} Combined prompt string
|
||||
*/
|
||||
messagesToPrompt(messages) {
|
||||
return messages.map(msg => {
|
||||
if (msg.role === 'user') {
|
||||
return msg.content;
|
||||
} else if (msg.role === 'assistant') {
|
||||
return `Assistant: ${msg.content}`;
|
||||
} else if (msg.role === 'system') {
|
||||
return `System: ${msg.content}`;
|
||||
}
|
||||
return msg.content;
|
||||
}).join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates content (non-streaming)
|
||||
* @param {string} model - Model name
|
||||
* @param {object} requestBody - Request body (Claude format)
|
||||
* @returns {Promise<object>} Claude API response (Claude compatible format)
|
||||
*/
|
||||
async generateContent(model, requestBody) {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
const prompt = this.messagesToPrompt(requestBody.messages);
|
||||
const output = await this.executeDroidCommand(['exec', prompt]);
|
||||
|
||||
return {
|
||||
id: `msg_${Date.now()}`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: output.trim()
|
||||
}],
|
||||
model: model,
|
||||
stop_reason: 'end_turn',
|
||||
usage: {
|
||||
input_tokens: Math.ceil(prompt.length / 4),
|
||||
output_tokens: Math.ceil(output.length / 4)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw this._handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams content generation
|
||||
* @param {string} model - Model name
|
||||
* @param {object} requestBody - Request body (Claude format)
|
||||
* @returns {AsyncIterable<object>} Claude API response stream (Claude compatible format)
|
||||
*/
|
||||
async *generateContentStream(model, requestBody) {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const prompt = this.messagesToPrompt(requestBody.messages);
|
||||
|
||||
yield {
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id: `msg_${Date.now()}`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
model: model
|
||||
}
|
||||
};
|
||||
|
||||
yield {
|
||||
type: 'content_block_start',
|
||||
index: 0,
|
||||
content_block: { type: 'text', text: '' }
|
||||
};
|
||||
|
||||
const droid = spawn(this.droidCommand, ['exec', prompt]);
|
||||
|
||||
let buffer = '';
|
||||
for await (const chunk of droid.stdout) {
|
||||
const text = chunk.toString();
|
||||
buffer += text;
|
||||
|
||||
yield {
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: { type: 'text_delta', text: text }
|
||||
};
|
||||
}
|
||||
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
index: 0
|
||||
};
|
||||
|
||||
yield {
|
||||
type: 'message_delta',
|
||||
delta: { stop_reason: 'end_turn' },
|
||||
usage: { output_tokens: Math.ceil(buffer.length / 4) }
|
||||
};
|
||||
|
||||
yield {
|
||||
type: 'message_stop'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available models
|
||||
* The Droid provider supports Claude models available through Factory.ai
|
||||
* @returns {Promise<object>} List of models
|
||||
*/
|
||||
async listModels() {
|
||||
console.log('[Droid] Listing available models.');
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 'claude-sonnet-4-5-20250929',
|
||||
object: 'model',
|
||||
created: 1725494400,
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-7-sonnet-20250219',
|
||||
object: 'model',
|
||||
created: 1708387200,
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
object: 'model',
|
||||
created: 1698019200,
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
object: 'model',
|
||||
created: 1709251200,
|
||||
owned_by: 'anthropic'
|
||||
}
|
||||
],
|
||||
object: 'list'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CLI errors
|
||||
* @param {Error} error - Error object
|
||||
* @returns {Error} Formatted error
|
||||
*/
|
||||
_handleError(error) {
|
||||
console.error('[Droid] Error:', error.message);
|
||||
return new Error(`Droid CLI error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
74
src/droid/droid-strategy.js
Normal file
74
src/droid/droid-strategy.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { ProviderStrategy } from '../provider-strategy.js';
|
||||
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../common.js';
|
||||
|
||||
/**
|
||||
* Droid (Factory.ai) provider strategy implementation
|
||||
* Follows Claude protocol since Droid uses Claude API
|
||||
*/
|
||||
class DroidStrategy extends ProviderStrategy {
|
||||
extractModelAndStreamInfo(req, requestBody) {
|
||||
const model = requestBody.model || 'claude-sonnet-4-5-20250929';
|
||||
const isStream = requestBody.stream === true;
|
||||
return { model, isStream };
|
||||
}
|
||||
|
||||
extractResponseText(response) {
|
||||
if (response.type === 'content_block_delta' && response.delta) {
|
||||
if (response.delta.type === 'text_delta') {
|
||||
return response.delta.text;
|
||||
}
|
||||
if (response.delta.type === 'input_json_delta') {
|
||||
return response.delta.partial_json;
|
||||
}
|
||||
}
|
||||
if (response.content && Array.isArray(response.content)) {
|
||||
return response.content
|
||||
.filter(block => block.type === 'text' && block.text)
|
||||
.map(block => block.text)
|
||||
.join('');
|
||||
} else if (response.content && response.content.type === 'text') {
|
||||
return response.content.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
extractPromptText(requestBody) {
|
||||
if (requestBody.messages && requestBody.messages.length > 0) {
|
||||
const lastMessage = requestBody.messages[requestBody.messages.length - 1];
|
||||
if (lastMessage.content && Array.isArray(lastMessage.content)) {
|
||||
return lastMessage.content.map(block => block.text || '').join('');
|
||||
}
|
||||
return lastMessage.content || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async applySystemPromptFromFile(config, requestBody) {
|
||||
if (!config.SYSTEM_PROMPT_FILE_PATH) {
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
const filePromptContent = config.SYSTEM_PROMPT_CONTENT;
|
||||
if (filePromptContent === null) {
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
const existingSystemText = extractSystemPromptFromRequestBody(requestBody, MODEL_PROTOCOL_PREFIX.CLAUDE);
|
||||
|
||||
const newSystemText = config.SYSTEM_PROMPT_MODE === 'append' && existingSystemText
|
||||
? `${existingSystemText}\n${filePromptContent}`
|
||||
: filePromptContent;
|
||||
|
||||
requestBody.system = newSystemText;
|
||||
console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'droid'.`);
|
||||
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
async manageSystemPrompt(requestBody) {
|
||||
const incomingSystemText = extractSystemPromptFromRequestBody(requestBody, MODEL_PROTOCOL_PREFIX.CLAUDE);
|
||||
await this._updateSystemPromptFile(incomingSystemText, 'droid');
|
||||
}
|
||||
}
|
||||
|
||||
export { DroidStrategy };
|
||||
|
|
@ -2,6 +2,7 @@ import { MODEL_PROTOCOL_PREFIX } from './common.js';
|
|||
import { GeminiStrategy } from './gemini/gemini-strategy.js';
|
||||
import { OpenAIStrategy } from './openai/openai-strategy.js';
|
||||
import { ClaudeStrategy } from './claude/claude-strategy.js';
|
||||
import { DroidStrategy } from './droid/droid-strategy.js';
|
||||
|
||||
/**
|
||||
* Strategy factory that returns the appropriate strategy instance based on the provider protocol.
|
||||
|
|
@ -15,6 +16,8 @@ class ProviderStrategyFactory {
|
|||
return new OpenAIStrategy();
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return new ClaudeStrategy();
|
||||
case MODEL_PROTOCOL_PREFIX.DROID:
|
||||
return new DroidStrategy(); // Droid uses Claude-compatible protocol
|
||||
default:
|
||||
throw new Error(`Unsupported provider protocol: ${providerProtocol}`);
|
||||
}
|
||||
|
|
|
|||
59
test-droid.js
Normal file
59
test-droid.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Test script for Droid provider
|
||||
import { DroidApiService } from './src/droid/droid-core.js';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
async function testDroidProvider() {
|
||||
console.log('🧪 Testing Droid Provider (CLI-based)...\n');
|
||||
|
||||
try {
|
||||
// Test 1: Initialize DroidApiService (check droid CLI availability)
|
||||
console.log('✅ Test 1: Initializing DroidApiService...');
|
||||
const service = new DroidApiService();
|
||||
await service.initialize();
|
||||
console.log(' DroidApiService initialized successfully');
|
||||
console.log(' isInitialized:', service.isInitialized);
|
||||
|
||||
// Test 2: List models
|
||||
console.log('\n✅ Test 2: Listing available models...');
|
||||
const models = await service.listModels();
|
||||
console.log(` Available models: ${models.data.length}`);
|
||||
models.data.forEach(model => {
|
||||
console.log(` - ${model.id}`);
|
||||
});
|
||||
|
||||
// Test 3: Test simple request (non-streaming)
|
||||
console.log('\n✅ Test 3: Testing simple request...');
|
||||
try {
|
||||
const response = await service.generateContent('claude-sonnet-4-5-20250929', {
|
||||
messages: [
|
||||
{ role: 'user', content: 'Say "Hello from Droid test!" in one sentence.' }
|
||||
],
|
||||
max_tokens: 50
|
||||
});
|
||||
console.log(' Response received:');
|
||||
console.log(' Model:', response.model);
|
||||
console.log(' Stop reason:', response.stop_reason);
|
||||
if (response.content && response.content[0]) {
|
||||
console.log(' Content:', response.content[0].text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' ❌ Request failed:', error.message);
|
||||
console.log(' 💡 Make sure you are authenticated with: droid');
|
||||
}
|
||||
|
||||
console.log('\n✅ All tests completed!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed:', error.message);
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testDroidProvider().then(() => {
|
||||
console.log('\n🎉 Test suite finished');
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('\n💥 Test suite error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in a new issue