feat: auto-detect dedicated memory llm providers

This commit is contained in:
Youzini-afk
2026-04-11 22:22:05 +08:00
parent 1033b56a07
commit 1a2e9ac33d
7 changed files with 646 additions and 50 deletions

View File

@@ -110,6 +110,23 @@ async function withModelFetchSettings(run) {
}
}
async function withModelFetchSettingsOverrides(overrides, run) {
const previousSettings = JSON.parse(
JSON.stringify(extensionsApi.extension_settings.st_bme || {}),
);
extensionsApi.extension_settings.st_bme = {
...previousSettings,
...buildModelFetchSettings(),
...(overrides || {}),
};
try {
await run();
} finally {
extensionsApi.extension_settings.st_bme = previousSettings;
}
}
async function testFetchMemoryModelsUsesCustomStatusFirst() {
const originalFetch = globalThis.fetch;
const seenBodies = [];
@@ -238,8 +255,70 @@ async function testFetchMemoryModelsParsesNestedPayload() {
}
}
async function testFetchMemoryModelsUsesGoogleStatusRoute() {
const originalFetch = globalThis.fetch;
const seenBodies = [];
globalThis.fetch = async (_url, options = {}) => {
seenBodies.push(JSON.parse(String(options.body || "{}")));
return new Response(
JSON.stringify({
data: [{ id: "gemini-2.5-pro" }],
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
},
);
};
try {
await withModelFetchSettingsOverrides(
{
llmApiUrl:
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent",
llmApiKey: "gemini-secret",
},
async () => {
const result = await llm.fetchMemoryLLMModels();
assert.equal(result.success, true);
assert.deepEqual(result.models, [
{ id: "gemini-2.5-pro", label: "gemini-2.5-pro" },
]);
assert.equal(seenBodies.length, 1);
assert.equal(seenBodies[0].chat_completion_source, "makersuite");
assert.equal(seenBodies[0].reverse_proxy, "https://generativelanguage.googleapis.com");
assert.equal(seenBodies[0].proxy_password, "gemini-secret");
},
);
} finally {
globalThis.fetch = originalFetch;
}
}
async function testFetchMemoryModelsReturnsHelpfulMessageForAnthropic() {
await withModelFetchSettingsOverrides(
{
llmApiUrl: "https://api.anthropic.com/v1/messages",
llmApiKey: "anthropic-secret",
llmModel: "claude-sonnet-4-5",
},
async () => {
const result = await llm.fetchMemoryLLMModels();
assert.equal(result.success, false);
assert.equal(result.models.length, 0);
assert.match(result.error, /Anthropic Claude/);
assert.match(result.error, /手动填写模型名/);
},
);
}
await testFetchMemoryModelsUsesCustomStatusFirst();
await testFetchMemoryModelsFallsBackToLegacyStatus();
await testFetchMemoryModelsParsesNestedPayload();
await testFetchMemoryModelsUsesGoogleStatusRoute();
await testFetchMemoryModelsReturnsHelpfulMessageForAnthropic();
console.log("llm-model-fetch tests passed");

View File

@@ -5,6 +5,7 @@ import {
isSameLlmConfigSnapshot,
isUsableLlmConfigSnapshot,
normalizeLlmPresetMap,
resolveDedicatedLlmProviderConfig,
resolveLlmConfigSelection,
resolveActiveLlmPresetName,
sanitizeLlmPresetSettings,
@@ -226,4 +227,31 @@ assert.deepEqual(invalidTaskPresetSelection.config, {
llmModel: "model-global",
});
const arkProvider = resolveDedicatedLlmProviderConfig(
"https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions",
);
assert.equal(arkProvider.providerId, "volcengine-ark");
assert.equal(arkProvider.transportId, "dedicated-openai-compatible");
assert.equal(arkProvider.routeMode, "custom");
assert.equal(arkProvider.apiUrl, "https://ark.cn-beijing.volces.com/api/coding/v3");
assert.equal(arkProvider.supportsModelFetch, true);
const anthropicProvider = resolveDedicatedLlmProviderConfig(
"https://api.anthropic.com/v1/messages",
);
assert.equal(anthropicProvider.providerId, "anthropic-claude");
assert.equal(anthropicProvider.transportId, "dedicated-anthropic-claude");
assert.equal(anthropicProvider.routeMode, "reverse-proxy");
assert.equal(anthropicProvider.apiUrl, "https://api.anthropic.com/v1");
assert.equal(anthropicProvider.supportsModelFetch, false);
const geminiProvider = resolveDedicatedLlmProviderConfig(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent",
);
assert.equal(geminiProvider.providerId, "google-ai-studio");
assert.equal(geminiProvider.transportId, "dedicated-google-ai-studio");
assert.equal(geminiProvider.routeMode, "reverse-proxy");
assert.equal(geminiProvider.apiUrl, "https://generativelanguage.googleapis.com");
assert.equal(geminiProvider.supportsModelFetch, true);
console.log("llm-preset-utils tests passed");

View File

@@ -83,7 +83,7 @@ if (originalSendOpenAIRequest === undefined) {
globalThis.__llmStreamingSendOpenAIRequest = originalSendOpenAIRequest;
}
function buildStreamingSettings(generation = {}) {
function buildStreamingSettings(generation = {}, overrides = {}) {
const taskProfiles = createDefaultTaskProfiles();
taskProfiles.extract.profiles[0].generation = {
...taskProfiles.extract.profiles[0].generation,
@@ -96,6 +96,7 @@ function buildStreamingSettings(generation = {}) {
timeoutMs: 1234,
taskProfilesVersion: 3,
taskProfiles,
...(overrides || {}),
};
}
@@ -125,13 +126,13 @@ function getSnapshot(taskKey = "extract") {
return globalThis.__stBmeRuntimeDebugState?.taskLlmRequests?.[taskKey] || null;
}
async function withStreamingSettings(generation, run) {
async function withStreamingSettings(generation, run, overrides = {}) {
const previousSettings = JSON.parse(
JSON.stringify(extensionsApi.extension_settings.st_bme || {}),
);
extensionsApi.extension_settings.st_bme = {
...previousSettings,
...buildStreamingSettings(generation),
...buildStreamingSettings(generation, overrides),
};
delete globalThis.__stBmeRuntimeDebugState;
@@ -415,9 +416,72 @@ async function testJsonRetryKeepsProfileCompletionTokens() {
}
}
async function testAnthropicRouteUsesReverseProxyAndDisablesStreaming() {
const originalFetch = globalThis.fetch;
let requestBody = null;
globalThis.fetch = async (_url, options = {}) => {
requestBody = JSON.parse(String(options.body || "{}"));
return new Response(
JSON.stringify({
choices: [
{
message: {
content: '{"ok":true}',
},
finish_reason: "stop",
},
],
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
},
);
};
try {
await withStreamingSettings(
{ stream: true },
async () => {
const result = await llm.callLLMForJSON({
systemPrompt: "system",
userPrompt: "user",
maxRetries: 0,
taskType: "extract",
requestSource: "test:anthropic-route",
});
assert.deepEqual(result, { ok: true });
assert.equal(requestBody?.chat_completion_source, "claude");
assert.equal(requestBody?.reverse_proxy, "https://api.anthropic.com/v1");
assert.equal(requestBody?.proxy_password, "sk-stream-secret");
assert.equal(requestBody?.stream, false);
assert.ok(requestBody?.json_schema);
const snapshot = getSnapshot("extract");
assert.ok(snapshot);
assert.equal(snapshot.route, "dedicated-anthropic-claude");
assert.equal(snapshot.llmProviderLabel, "Anthropic Claude");
assert.equal(snapshot.streamRequested, false);
assert.equal(snapshot.streamForceDisabled, true);
},
{
llmApiUrl: "https://api.anthropic.com/v1/messages",
llmModel: "claude-sonnet-4-5",
},
);
} finally {
globalThis.fetch = originalFetch;
}
}
await testDedicatedStreamingSuccess();
await testDedicatedStreamingFallsBackToNonStream();
await testDedicatedStreamingAbortDoesNotLeaveActiveState();
await testJsonRetryKeepsProfileCompletionTokens();
await testAnthropicRouteUsesReverseProxyAndDisablesStreaming();
console.log("llm-streaming tests passed");