v2.2.19: Cloud Model Providers Support (OpenRouter, Anthropic, Gemini)

This commit is contained in:
g1nation
2026-05-16 23:34:35 +09:00
parent c7b596f17a
commit 88664c7c6e
21 changed files with 1154 additions and 46 deletions
+78 -2
View File
@@ -731,8 +731,16 @@ export class AgentExecutor {
this.abortController?.abort();
}, timeout);
const engine = resolveEngine(ollamaUrl);
const useLmStudioSdk = engine === 'lmstudio' && !!this.options.lmStudioStreamer;
// Cloud provider 라우팅 — actualModel 의 prefix 가 cloud 면 SDK / 로컬 REST 경로 둘 다 우회.
// SSE 파서 입장에서는 동일한 OpenAI 호환 stream 이 들어오므로 consumer 변경 없음.
const _cloudHit = (() => {
try {
const { parseModelPrefix } = require('./features/providers') as typeof import('./features/providers');
return parseModelPrefix(actualModel);
} catch { return null; }
})();
const engine = _cloudHit ? 'lmstudio' : resolveEngine(ollamaUrl);
const useLmStudioSdk = !_cloudHit && engine === 'lmstudio' && !!this.options.lmStudioStreamer;
let apiUrl = '';
let aiResponseText = '';
let buffer = '';
@@ -2754,6 +2762,35 @@ export class AgentExecutor {
}): Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }> {
const { baseUrl, modelName, reqMessages, temperature } = params;
const maxTokens = Math.max(256, params.maxTokens ?? 4096);
// Cloud provider 라우팅 — model id 가 'openrouter:' / 'anthropic:' / 'gemini:' 로 시작하면
// 해당 adapter 호출. body 는 OpenAI 호환 SSE 로 transform 되어 반환되므로
// 아래 로컬 엔진 경로의 consumer 가 동일하게 처리.
try {
const { parseModelPrefix, streamCloudCompletion } =
require('./features/providers') as typeof import('./features/providers');
const hit = parseModelPrefix(modelName);
if (hit) {
logInfo('AI streaming request (cloud).', { provider: hit.provider, model: hit.model });
const response = await streamCloudCompletion(this.context, hit, {
messages: reqMessages.map((m) => ({ role: m.role as any, content: m.content })),
temperature,
maxTokens,
signal: this.abortController?.signal,
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Cloud (${hit.provider}) ${response.status}: ${summarizeText(errText, 300)}`);
}
return { response, engine: 'lmstudio', apiUrl: `cloud://${hit.provider}/${hit.model}` };
}
} catch (e) {
// 모듈 로드 실패 / 매칭 안 됨 — 로컬 경로로 fall through.
// (단, 명시적으로 cloud routing 했는데 실패한 경우는 throw 되어 위에서 catch 됨.)
const msg = (e as Error)?.message ?? '';
if (msg.startsWith('Cloud (')) throw e;
}
const numCtx = Math.max(2048, params.contextLength ?? 32768);
const engine = resolveEngine(baseUrl); // 사용자가 설정한 엔진만 사용
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
@@ -2860,6 +2897,45 @@ export class AgentExecutor {
}): Promise<{ text: string; stopReason?: string }> {
const { baseUrl, modelName, engine, messages, temperature, signal } = params;
const maxTokens = Math.max(256, params.maxTokens ?? 4096);
// Cloud routing — streaming Response 를 받아 끝까지 모아서 텍스트로 환원.
// Non-streaming 전용 endpoint 를 따로 두지 않고 stream 결과를 모으는 게 단순.
try {
const { parseModelPrefix, streamCloudCompletion } =
require('./features/providers') as typeof import('./features/providers');
const hit = parseModelPrefix(modelName);
if (hit) {
const response = await streamCloudCompletion(this.context, hit, {
messages: messages.map((m) => ({ role: m.role as any, content: m.content })),
temperature,
maxTokens,
signal,
});
if (!response.ok) {
const errText = await response.text().catch(() => '');
throw new Error(`Cloud (${hit.provider}) ${response.status}: ${summarizeText(errText, 200)}`);
}
// OpenAI 호환 SSE 를 통째로 읽어 delta.content 합치기.
const raw = await response.text();
let acc = '';
for (const line of raw.split('\n')) {
const t = line.trim();
if (!t.startsWith('data:')) continue;
const payload = t.slice(5).trim();
if (!payload || payload === '[DONE]') continue;
try {
const obj = JSON.parse(payload);
const delta = obj?.choices?.[0]?.delta?.content;
if (typeof delta === 'string') acc += delta;
} catch { /* skip malformed */ }
}
return { text: acc, stopReason: 'stop' };
}
} catch (e) {
const msg = (e as Error)?.message ?? '';
if (msg.startsWith('Cloud (')) throw e;
}
const numCtx = Math.max(2048, params.contextLength ?? 32768);
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
const variants = this.buildEngineMessageVariants(messages, engine);