mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-14 09:01:22 +00:00
fix(dsh): read the protocol table by its own entries
The LLM protocol table is a plain object literal, so it inherits from Object.prototype: `LLM_PROTOCOLS["constructor"]` and `["__proto__"]` answered with the inherited members instead of `undefined`. A backend named that way was therefore not treated as an unknown name: the probe built a malformed request from `Object` and failed with a generic "no result", while the upper-case spellings were reported as known backends written in the wrong case, naming a spelling ReMe refuses too. Both readers of the table now consult its own entries only, through one helper, so "what the plugin knows" has a single answer: such a name is probed tolerantly like any other unknown name, and a name that really is a known backend in the wrong case is still refused with the spelling to use.
This commit is contained in:
parent
e9e572cddd
commit
3d52f61da1
4 changed files with 79 additions and 5 deletions
|
|
@ -136,7 +136,8 @@ DSH settings.
|
|||
backend, exactly as ReMe would start it.
|
||||
- A known backend written in the wrong case (`LLM_BACKEND=OpenAI`) is reported as invalid, with the spelling to
|
||||
use: ReMe's backend registry is case-sensitive and refuses that name. A backend name the plugin has never heard
|
||||
of is still probed tolerantly — the list here is a snapshot, and ReMe may know more than the plugin does.
|
||||
of is still probed tolerantly — the list here is a snapshot, and ReMe may know more than the plugin does. That
|
||||
holds however the name looks: only a backend the plugin actually knows can be reported as a wrong spelling.
|
||||
- The draft, the save message and the LLM-test result belong to the workspace they were produced for: switching
|
||||
the workspace disposes the section's state, so neither a key typed for one workspace nor a success reported for
|
||||
it can be carried into another.
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ ReMe 在服务启动时读取 workspace 目录下的 `.env`;标签页从不把
|
|||
任何请求:此时 ReMe 启动时并没有任何后端,探测不能去测试服务根本无法使用的配置。若 `.env` 里没有这个键,
|
||||
探测会与 ReMe 一样使用默认后端。
|
||||
- 已知后端名写错了大小写(如 `LLM_BACKEND=OpenAI`)会被报告为无效,并给出正确写法:ReMe 的后端注册表区分大小写,
|
||||
会拒绝这个名字。而插件从未听说过的后端名仍被宽容地探测——这里的列表只是一份快照,ReMe 可能认识得更多。
|
||||
会拒绝这个名字。而插件从未听说过的后端名仍被宽容地探测——这里的列表只是一份快照,ReMe 可能认识得更多。这条规则与名字
|
||||
长什么样无关:只有插件确实认识的后端才会被判定为大小写写错。
|
||||
- 草稿、保存提示与 LLM 测试结果都属于产生它们的 workspace:切换 workspace 会随旧区块一并丢弃,因此为某个 workspace
|
||||
输入的密钥、或属于它的一次成功提示,都不会被带到另一个。
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,17 @@ export const LLM_PROTOCOLS: Record<string, LlmProtocol> = {
|
|||
/** The backend ReMe itself falls back to when `.env` names none. */
|
||||
export { DEFAULT_LLM_BACKEND };
|
||||
|
||||
/**
|
||||
* The protocol entry the table holds for `key`, if it holds one at all.
|
||||
*
|
||||
* Only an own entry counts: the table inherits from `Object.prototype`, so a
|
||||
* plain lookup would hand out `Object` for a backend named `constructor` and
|
||||
* build a malformed probe from it.
|
||||
*/
|
||||
function ownProtocol(key: string): LlmProtocol | undefined {
|
||||
return Object.hasOwn(LLM_PROTOCOLS, key) ? LLM_PROTOCOLS[key] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The protocol a backend speaks; an unknown backend uses the OpenAI-style
|
||||
* request.
|
||||
|
|
@ -287,7 +298,7 @@ export { DEFAULT_LLM_BACKEND };
|
|||
* refused.
|
||||
*/
|
||||
export function llmProtocolFor(backend: string): LlmProtocol {
|
||||
return LLM_PROTOCOLS[llmBackendOf(backend)] ?? OPENAI_PROTOCOL;
|
||||
return ownProtocol(llmBackendOf(backend)) ?? OPENAI_PROTOCOL;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -299,9 +310,12 @@ export function llmProtocolFor(backend: string): LlmProtocol {
|
|||
* alone: its list is a snapshot, and ReMe may know more than the plugin does.
|
||||
*/
|
||||
export function backendSpellingIsWrong(name: string): boolean {
|
||||
// Own entries only. An inherited member is not a known backend: `constructor`
|
||||
// and `__proto__` would otherwise be reported as known backends written in
|
||||
// the wrong case, and the message would name a spelling ReMe refuses too.
|
||||
return (
|
||||
LLM_PROTOCOLS[name] === undefined &&
|
||||
LLM_PROTOCOLS[name.toLowerCase()] !== undefined
|
||||
ownProtocol(name) === undefined &&
|
||||
ownProtocol(name.toLowerCase()) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -526,6 +526,64 @@ test("the probe refuses a known backend written in the wrong case", async (t) =>
|
|||
assert.equal(calls.length, 0, "the snippet is not run");
|
||||
});
|
||||
|
||||
test("a backend name the plugin does not know is probed tolerantly, whatever it looks like", async (t) => {
|
||||
// The protocol table is a plain object, so it inherits from
|
||||
// `Object.prototype`: `constructor` and `__proto__` are not backends. A name
|
||||
// the plugin has never heard of must be probed like any other unknown name,
|
||||
// and must never be reported as a known backend written in the wrong case.
|
||||
const directory = mkdtempSync(join(tmpdir(), "reme-llm-prototype-"));
|
||||
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
||||
const names = ["constructor", "__proto__", "CONSTRUCTOR", "__PROTO__"];
|
||||
for (const name of names) {
|
||||
writeFileSync(
|
||||
join(directory, ".env"),
|
||||
[
|
||||
`LLM_BACKEND=${name}`,
|
||||
"LLM_MODEL_NAME=test-model",
|
||||
"LLM_API_KEY=sk-test",
|
||||
"LLM_BASE_URL=https://provider.invalid",
|
||||
].join("\n"),
|
||||
);
|
||||
const calls = [];
|
||||
const result = await testLlm(
|
||||
{
|
||||
run: async (exe, args, options) => {
|
||||
calls.push({ exe, args, options });
|
||||
return {
|
||||
code: 0,
|
||||
stdout:
|
||||
'{"ok": true, "status": 200, "latencyMs": 12, "error": ""}\n',
|
||||
stderr: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
{ workspaceDir: directory, pythonPath: "C:\\env\\python.exe" },
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
result.error ?? "",
|
||||
/lower case/,
|
||||
`${name} is not reported as a known backend in the wrong case`,
|
||||
);
|
||||
assert.equal(calls.length, 1, `${name} is probed tolerantly`);
|
||||
const snippet = String(calls[0]?.args[1] ?? "");
|
||||
assert.match(
|
||||
snippet,
|
||||
/\/chat\/completions/,
|
||||
`${name} gets the default protocol`,
|
||||
);
|
||||
assert.match(
|
||||
snippet,
|
||||
/"Authorization"/,
|
||||
`${name} gets the default protocol`,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
snippet,
|
||||
/undefined/,
|
||||
`${name} builds no malformed snippet`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("a .env without the backend key is probed with the default protocol", async (t) => {
|
||||
// The key is absent, so ReMe applies its default; the probe does the same.
|
||||
const directory = mkdtempSync(join(tmpdir(), "reme-llm-absent-"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue