v2.2.19: Cloud Model Providers Support (OpenRouter, Anthropic, Gemini)
This commit is contained in:
+78
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user