Extend star-cliproxy with custom providers — image generators, custom LLM APIs, or any HTTP-based service — without modifying the main codebase.
plugins/
my-provider/
index.js # CliproxyPlugin interface implementation
package.json # (optional) if the plugin has dependencies
Each subdirectory under plugins/ is treated as a potential plugin. Failed plugins are skipped — the server starts normally without them.
interface CliproxyPlugin {
// Unique plugin name
name: string;
// Endpoint types this plugin handles
endpointTypes: Array<'chat' | 'images' | 'tts' | 'embeddings'>;
// Factory: receives config from config.yaml, returns a provider instance
createProvider(config: Record<string, unknown>): {
name: string;
execute(options: ExecuteOptions): Promise<ExecuteResult>;
checkHealth(): Promise<'healthy' | 'unhealthy' | 'unknown'>;
};
// (optional) Custom streaming parser — defaults to PlainTextParser
// createParser?(): { parse(line: string): string | null };
}
interface ExecuteOptions {
messages: Array<{ role: string; content: string }>;
model?: string;
stream?: boolean;
// ...other OpenAI-compatible fields
}
interface ExecuteResult {
content: string;
usage: { promptTokens: number; completionTokens: number; totalTokens: number };
finishReason: 'stop' | 'length' | 'error';
}| Type | Description | API Path | Status |
|---|---|---|---|
chat |
Text conversation | /v1/chat/completions |
Supported |
images |
Image generation | /v1/images/generations |
Supported |
tts |
Text-to-speech | /v1/audio/speech |
Planned |
embeddings |
Embeddings | /v1/embeddings |
Planned |
// plugins/my-provider/index.js
export default {
name: 'my-provider',
endpointTypes: ['chat'],
createProvider(config) {
return {
name: 'my-provider',
async execute(options) {
const lastMessage = options.messages.at(-1)?.content ?? '';
// Implement: CLI call, HTTP request, or any logic
const responseText = `Echo: ${lastMessage}`;
return {
content: responseText,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
finishReason: 'stop',
};
},
async checkHealth() {
return 'healthy';
},
};
},
};For image generation plugins, the execute method must return an OpenAI Images API-compatible JSON string as content.
// plugins/my-image-provider/index.js
export default {
name: 'my-image-provider',
endpointTypes: ['images'],
createProvider(config) {
const cliPath = config.cli_path ?? 'my-cli';
const timeoutMs = config.timeout_ms ?? 120000;
return {
name: 'my-image-provider',
async execute(options) {
// The prompt arrives as the last message content
const prompt = options.messages.at(-1)?.content ?? '';
// --- Option A: spawn a CLI tool ---
// const { stdout } = await runCli(cliPath, ['generate', prompt], timeoutMs);
// const imageUrl = parseUrl(stdout);
// --- Option B: call an HTTP API ---
// const res = await fetch('https://api.example.com/generate', {
// method: 'POST',
// body: JSON.stringify({ prompt }),
// });
// const { url: imageUrl } = await res.json();
const imageUrl = 'https://example.com/generated-image.png';
// Must return OpenAI Images API format
const openaiResponse = {
created: Math.floor(Date.now() / 1000),
data: [{ url: imageUrl }],
};
return {
content: JSON.stringify(openaiResponse),
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
finishReason: 'stop',
};
},
async checkHealth() {
return 'healthy';
},
};
},
};The proxy returns the content string directly as the HTTP response body for /v1/images/generations, so it must be valid JSON matching the OpenAI Images API format:
{
"created": 1234567890,
"data": [{ "url": "https://..." }]
}plugins:
- path: "./plugins/my-image-provider"
config:
cli_path: "my-cli"
default_model: "my-model"
timeout_ms: 120000
- path: "./plugins/another-provider"
config:
api_key_env: "MY_API_KEY"The config object is passed as-is to createProvider(config).
- Server starts and reads
pluginsfromconfig.yaml - Each entry's
pathis resolved relative to the project root - The
index.js(orindex.mjs) is imported via dynamicimport() createProvider(config)is called with the entry'sconfigobject- The resulting provider is registered in the provider engine
- Model mappings in the dashboard can target plugin providers by name
- Requests routed to the plugin go through the standard auth → rate-limit → cache pipeline
createProvider(config) {
const apiKey = process.env[config.api_key_env ?? 'MY_API_KEY'];
if (!apiKey) throw new Error('API key env var not set');
return {
name: 'my-provider',
async execute(options) {
// use apiKey
},
async checkHealth() { return 'healthy'; },
};
},createProvider(config) {
let cachedToken = null;
async function getToken() {
if (cachedToken) return cachedToken;
// e.g. read from ~/.config/my-cli/token
cachedToken = await readTokenFromFile();
return cachedToken;
}
return {
name: 'my-provider',
async execute(options) {
const token = await getToken();
// use token
},
async checkHealth() {
try { await getToken(); return 'healthy'; }
catch { return 'unhealthy'; }
},
};
},# 1. Register the plugin in config.yaml (see above)
# 2. Add a model mapping in the dashboard pointing to your plugin
# 3. Use Test Model on the mapping to validate before saving
# 4. Call the endpoint directly:
curl http://localhost:8300/v1/images/generations \
-H "Authorization: Bearer sk-proxy-your-secret-key" \
-H "Content-Type: application/json" \
-d '{
"model": "my-image-model",
"messages": [{"role": "user", "content": "A sunset over the ocean"}]
}'Image URLs returned in debug logs and test results are shown as clickable previews in the dashboard.
plugins/*/is gitignored by default — add personal plugins freely- Plugins must provide built
.jsfiles; TypeScript must be compiled before use - Plugin load failure is isolated — only that plugin is skipped, the server starts normally
- Plugins go through the same auth, rate-limit, and cache pipeline as built-in providers
- The
example-plugin/directory contains a working reference implementation
메인 코드 수정 없이 커스텀 프로바이더를 star-cliproxy에 추가할 수 있습니다. 이미지 생성기, 커스텀 LLM API, HTTP 기반 서비스 등을 플러그인으로 연동하세요.
plugins/
my-provider/
index.js # CliproxyPlugin 인터페이스 구현
package.json # (선택) 의존성이 있는 경우
plugins/ 하위의 각 디렉토리가 플러그인 후보로 처리됩니다. 로드에 실패한 플러그인은 건너뛰고, 서버는 정상 시작합니다.
interface CliproxyPlugin {
// 고유한 플러그인 이름
name: string;
// 이 플러그인이 처리하는 엔드포인트 타입
endpointTypes: Array<'chat' | 'images' | 'tts' | 'embeddings'>;
// 팩토리: config.yaml의 config를 받아 프로바이더 인스턴스 반환
createProvider(config: Record<string, unknown>): {
name: string;
execute(options: ExecuteOptions): Promise<ExecuteResult>;
checkHealth(): Promise<'healthy' | 'unhealthy' | 'unknown'>;
};
// (선택) 커스텀 스트리밍 파서 — 없으면 PlainTextParser 사용
// createParser?(): { parse(line: string): string | null };
}| 타입 | 설명 | API 경로 | 상태 |
|---|---|---|---|
chat |
텍스트 대화 | /v1/chat/completions |
지원 |
images |
이미지 생성 | /v1/images/generations |
지원 |
tts |
음성 합성 | /v1/audio/speech |
예정 |
embeddings |
임베딩 | /v1/embeddings |
예정 |
// plugins/my-provider/index.js
export default {
name: 'my-provider',
endpointTypes: ['chat'],
createProvider(config) {
return {
name: 'my-provider',
async execute(options) {
const lastMessage = options.messages.at(-1)?.content ?? '';
// CLI 호출, HTTP 요청 등 자유롭게 구현
const responseText = `Echo: ${lastMessage}`;
return {
content: responseText,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
finishReason: 'stop',
};
},
async checkHealth() {
return 'healthy';
},
};
},
};이미지 생성 플러그인의 execute 메서드는 OpenAI Images API 호환 JSON 문자열을 content로 반환해야 합니다.
// plugins/my-image-provider/index.js
export default {
name: 'my-image-provider',
endpointTypes: ['images'],
createProvider(config) {
const cliPath = config.cli_path ?? 'my-cli';
const timeoutMs = config.timeout_ms ?? 120000;
return {
name: 'my-image-provider',
async execute(options) {
// 프롬프트는 마지막 메시지의 content로 전달됩니다
const prompt = options.messages.at(-1)?.content ?? '';
// --- 방법 A: CLI 도구 실행 ---
// const { stdout } = await runCli(cliPath, ['generate', prompt], timeoutMs);
// const imageUrl = parseUrl(stdout);
// --- 방법 B: HTTP API 호출 ---
// const res = await fetch('https://api.example.com/generate', {
// method: 'POST',
// body: JSON.stringify({ prompt }),
// });
// const { url: imageUrl } = await res.json();
const imageUrl = 'https://example.com/generated-image.png';
// OpenAI Images API 형식으로 반환 필수
const openaiResponse = {
created: Math.floor(Date.now() / 1000),
data: [{ url: imageUrl }],
};
return {
content: JSON.stringify(openaiResponse),
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
finishReason: 'stop',
};
},
async checkHealth() {
return 'healthy';
},
};
},
};프록시는 /v1/images/generations 응답으로 content 문자열을 그대로 반환하므로, 반드시 OpenAI Images API 형식의 유효한 JSON이어야 합니다:
{
"created": 1234567890,
"data": [{ "url": "https://..." }]
}plugins:
- path: "./plugins/my-image-provider"
config:
cli_path: "my-cli"
default_model: "my-model"
timeout_ms: 120000
- path: "./plugins/another-provider"
config:
api_key_env: "MY_API_KEY"config 객체는 createProvider(config)에 그대로 전달됩니다.
- 서버 시작 시
config.yaml의plugins섹션을 읽음 - 각 항목의
path를 프로젝트 루트 기준으로 resolve index.js(또는index.mjs)를 동적import()로 로드- 항목의
config객체를 인자로createProvider(config)호출 - 반환된 프로바이더를 프로바이더 엔진에 등록
- 대시보드 모델 매핑에서 플러그인 프로바이더를 이름으로 지정 가능
- 플러그인으로 라우팅된 요청은 기본 제공 프로바이더와 동일하게 auth → rate-limit → cache 파이프라인 통과
createProvider(config) {
const apiKey = process.env[config.api_key_env ?? 'MY_API_KEY'];
if (!apiKey) throw new Error('API 키 환경 변수가 설정되지 않았습니다');
return {
name: 'my-provider',
async execute(options) {
// apiKey 사용
},
async checkHealth() { return 'healthy'; },
};
},createProvider(config) {
let cachedToken = null;
async function getToken() {
if (cachedToken) return cachedToken;
// 예: ~/.config/my-cli/token 파일에서 읽기
cachedToken = await readTokenFromFile();
return cachedToken;
}
return {
name: 'my-provider',
async execute(options) {
const token = await getToken();
// token 사용
},
async checkHealth() {
try { await getToken(); return 'healthy'; }
catch { return 'unhealthy'; }
},
};
},# 1. config.yaml에 플러그인 등록 (위 설정 참고)
# 2. 대시보드에서 플러그인 프로바이더를 지정하는 모델 매핑 추가
# 3. 매핑 저장 전 Test Model로 검증
# 4. 엔드포인트 직접 호출:
curl http://localhost:8300/v1/images/generations \
-H "Authorization: Bearer sk-proxy-your-secret-key" \
-H "Content-Type: application/json" \
-d '{
"model": "my-image-model",
"messages": [{"role": "user", "content": "바다 위의 노을"}]
}'디버그 로그와 테스트 결과에서 반환된 이미지 URL은 대시보드에서 클릭 가능한 미리보기로 표시됩니다.
plugins/*/는 기본적으로 gitignore 처리 — 개인용 플러그인을 자유롭게 추가 가능- 플러그인은 빌드된
.js파일을 제공해야 함 (TypeScript는 미리 컴파일) - 플러그인 로드 실패는 해당 플러그인만 건너뛰며 서버는 정상 시작
- 플러그인은 기본 제공 프로바이더와 동일한 auth, rate-limit, cache 파이프라인 적용
example-plugin/디렉토리에 동작하는 참조 구현체가 있음