mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-30 17:22:02 +08:00
fix(core): stop falling back to delisted OpenRouter checkModel (#300)
- openrouter checkModel 换成长期存在的 openrouter/auto,静态清单删除已下架的 google/gemma-2-9b-it:free - 动态模型清单的服务(openrouter/newapi/kkaiapi/ppio/siliconcloud,原有 ollama)放行未列出的模型 id, 用户显式配置的模型不再被静默替换成 checkModel - 严格服务的模型归属校验行为不变
This commit is contained in:
@@ -374,6 +374,70 @@ describe("resolveEffectiveLLMConfig", () => {
|
||||
})).rejects.toThrow(/模型.*kimi-k2\.5.*不属于.*google/);
|
||||
});
|
||||
|
||||
it("openrouter 配置了不在静态 bank 的模型时保留用户模型,不回退 checkModel(issue #300)", async () => {
|
||||
await writeProject({
|
||||
configSource: "studio",
|
||||
service: "openrouter",
|
||||
services: [{ service: "openrouter" }],
|
||||
defaultModel: "moonshotai/kimi-k2-thinking",
|
||||
});
|
||||
await writeSecrets({ openrouter: { apiKey: "sk-or" } });
|
||||
|
||||
const result = await resolveEffectiveLLMConfig({
|
||||
consumer: "studio",
|
||||
projectRoot: root,
|
||||
envLayers: { global: {}, project: {}, process: {} },
|
||||
requireApiKey: true,
|
||||
});
|
||||
|
||||
expect(result.llm.service).toBe("openrouter");
|
||||
expect(result.llm.baseUrl).toBe("https://openrouter.ai/api/v1");
|
||||
expect(result.llm.model).toBe("moonshotai/kimi-k2-thinking");
|
||||
});
|
||||
|
||||
it("CLI env 显式指定 openrouter 未列出模型时也直接透传", async () => {
|
||||
await writeProject({
|
||||
configSource: "studio",
|
||||
service: "openrouter",
|
||||
services: [{ service: "openrouter" }],
|
||||
defaultModel: "openrouter/auto",
|
||||
});
|
||||
await writeSecrets({ openrouter: { apiKey: "sk-or" } });
|
||||
|
||||
const result = await resolveEffectiveLLMConfig({
|
||||
consumer: "cli",
|
||||
projectRoot: root,
|
||||
envLayers: {
|
||||
global: {},
|
||||
project: { INKOS_LLM_MODEL: "z-ai/glm-4.7" },
|
||||
process: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llm.service).toBe("openrouter");
|
||||
expect(result.llm.model).toBe("z-ai/glm-4.7");
|
||||
expect(result.diagnostics.modelSource).toBe("env");
|
||||
});
|
||||
|
||||
it("openrouter 完全没配模型时才回退到 checkModel", async () => {
|
||||
await writeProject({
|
||||
configSource: "studio",
|
||||
service: "openrouter",
|
||||
services: [{ service: "openrouter" }],
|
||||
});
|
||||
await writeSecrets({ openrouter: { apiKey: "sk-or" } });
|
||||
|
||||
const result = await resolveEffectiveLLMConfig({
|
||||
consumer: "studio",
|
||||
projectRoot: root,
|
||||
envLayers: { global: {}, project: {}, process: {} },
|
||||
requireApiKey: true,
|
||||
});
|
||||
|
||||
expect(result.llm.service).toBe("openrouter");
|
||||
expect(result.llm.model).toBe("openrouter/auto");
|
||||
});
|
||||
|
||||
it("CLI env 指向 Ollama 时允许用户本地安装的动态模型", async () => {
|
||||
await writeProject({
|
||||
configSource: "studio",
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("Layer 2 优先级(聚合入口精简后)", () => {
|
||||
});
|
||||
|
||||
it("OpenRouter 专属带后缀 id(:free)命中 openrouter provider", () => {
|
||||
const hit = lookupModel("custom", "google/gemma-2-9b-it:free");
|
||||
const hit = lookupModel("custom", "meta-llama/llama-3.1-8b-instruct:free");
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.maxOutput).toBe(4096);
|
||||
});
|
||||
|
||||
@@ -18,7 +18,9 @@ export const OPENROUTER: InkosEndpoint = {
|
||||
group: "aggregator",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
checkModel: "google/gemma-2-9b-it:free",
|
||||
// openrouter/auto 是 OpenRouter 官方的自动路由入口,长期存在;
|
||||
// 具体模型 id(如 google/gemma-2-9b-it:free)会随上游下架失效(issue #300)。
|
||||
checkModel: "openrouter/auto",
|
||||
temperatureRange: [0, 2],
|
||||
defaultTemperature: 0.7,
|
||||
writingTemperature: 1,
|
||||
@@ -82,6 +84,5 @@ export const OPENROUTER: InkosEndpoint = {
|
||||
{ id: "meta-llama/llama-3.3-70b-instruct:free", maxOutput: 4096, contextWindowTokens: 65536 },
|
||||
{ id: "qwen/qwen-2-7b-instruct:free", maxOutput: 4096, contextWindowTokens: 32768 },
|
||||
{ id: "meta-llama/llama-3.1-8b-instruct:free", maxOutput: 4096, contextWindowTokens: 131072 },
|
||||
{ id: "google/gemma-2-9b-it:free", maxOutput: 4096, contextWindowTokens: 8192 },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -464,8 +464,27 @@ function modelBelongsToService(service: string, model: string): boolean {
|
||||
return endpoint.models.some((knownModel) => knownModel.id.toLowerCase() === model.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 这些服务的可用模型清单是动态的,静态 bank 必然滞后,
|
||||
* 所以用户显式配置的模型 id 直接透传,不做 bank 白名单校验(issue #300):
|
||||
* - ollama:本地装了什么模型就有什么模型
|
||||
* - openrouter:聚合 350+ 上游模型,上游随时增删,bank 只列常用子集
|
||||
* - newapi:自建中转网关,模型清单由部署方自定义,bank 为空
|
||||
* - kkaiapi:多家大模型的 OpenAI 兼容中转站,上游清单随时变化
|
||||
* - ppio:聚合平台,每周都有新模型 id,bank 只维护主流子集
|
||||
* - siliconcloud:聚合平台,新模型上架频繁,bank 只维护主流子集
|
||||
*/
|
||||
const SERVICES_WITH_DYNAMIC_MODELS: ReadonlySet<string> = new Set([
|
||||
"ollama",
|
||||
"openrouter",
|
||||
"newapi",
|
||||
"kkaiapi",
|
||||
"ppio",
|
||||
"siliconcloud",
|
||||
]);
|
||||
|
||||
function serviceAllowsUnlistedModels(service: string): boolean {
|
||||
return service === "ollama";
|
||||
return SERVICES_WITH_DYNAMIC_MODELS.has(service);
|
||||
}
|
||||
|
||||
function serviceEntryKey(entry: ServiceConfigEntry): string {
|
||||
|
||||
Reference in New Issue
Block a user