From 3111119f93b7c2bb542115d6723e0f4716a679d4 Mon Sep 17 00:00:00 2001 From: bee4come Date: Wed, 15 Oct 2025 11:29:17 +0800 Subject: [PATCH] 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 --- README.md | 21 +++- src/adapter.js | 44 +++++++ src/common.js | 2 + src/droid/README.md | 227 +++++++++++++++++++++++++++++++++++ src/droid/droid-core.js | 229 ++++++++++++++++++++++++++++++++++++ src/droid/droid-strategy.js | 74 ++++++++++++ src/provider-strategies.js | 3 + test-droid.js | 59 ++++++++++ 8 files changed, 657 insertions(+), 2 deletions(-) create mode 100644 src/droid/README.md create mode 100644 src/droid/droid-core.js create mode 100644 src/droid/droid-strategy.js create mode 100644 test-droid.js diff --git a/README.md b/README.md index c0e57e2..9ea427a 100644 --- a/README.md +++ b/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 diff --git a/src/adapter.js b/src/adapter.js index c295e2a..15ee613 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -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}`); } diff --git a/src/common.js b/src/common.js index b677d9a..91b8a2e 100644 --- a/src/common.js +++ b/src/common.js @@ -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', } /** diff --git a/src/droid/README.md b/src/droid/README.md new file mode 100644 index 0000000..abe5601 --- /dev/null +++ b/src/droid/README.md @@ -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). diff --git a/src/droid/droid-core.js b/src/droid/droid-core.js new file mode 100644 index 0000000..cd07e89 --- /dev/null +++ b/src/droid/droid-core.js @@ -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} 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} args - Command arguments + * @returns {Promise} 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} 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} 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} 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} 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}`); + } +} diff --git a/src/droid/droid-strategy.js b/src/droid/droid-strategy.js new file mode 100644 index 0000000..e2bfb6a --- /dev/null +++ b/src/droid/droid-strategy.js @@ -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 }; diff --git a/src/provider-strategies.js b/src/provider-strategies.js index b1146ba..077d28c 100644 --- a/src/provider-strategies.js +++ b/src/provider-strategies.js @@ -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}`); } diff --git a/test-droid.js b/test-droid.js new file mode 100644 index 0000000..fde17f6 --- /dev/null +++ b/test-droid.js @@ -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); +});