mirror of https://github.com/openclaw/openclaw.git
fix(session-status): infer custom runtime providers from config (#58474)
* fix(session-status): infer custom runtime providers from config * test(session-status): satisfy custom provider type checks
This commit is contained in:
parent
547154865b
commit
40b24dfa6b
|
|
@ -284,7 +284,7 @@ describe("model-selection", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
|
|
@ -304,7 +304,7 @@ describe("model-selection", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
|
|
@ -323,7 +323,7 @@ describe("model-selection", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
|
|
@ -342,7 +342,7 @@ describe("model-selection", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
|
|
@ -351,6 +351,47 @@ describe("model-selection", () => {
|
|||
}),
|
||||
).toBe("vercel-ai-gateway");
|
||||
});
|
||||
|
||||
it("infers provider from configured provider catalogs when allowlist is absent", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
"qwen-dashscope": {
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: "qwen-max",
|
||||
}),
|
||||
).toBe("qwen-dashscope");
|
||||
});
|
||||
|
||||
it("returns undefined when provider catalog matches are ambiguous", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
"qwen-dashscope": {
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
modelstudio: {
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: "qwen-max",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildModelAliasIndex", () => {
|
||||
|
|
|
|||
|
|
@ -219,25 +219,52 @@ export function inferUniqueProviderFromConfiguredModels(params: {
|
|||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
const configuredModels = params.cfg.agents?.defaults?.models;
|
||||
if (!configuredModels) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = model.toLowerCase();
|
||||
const providers = new Set<string>();
|
||||
for (const key of Object.keys(configuredModels)) {
|
||||
const ref = key.trim();
|
||||
if (!ref || !ref.includes("/")) {
|
||||
continue;
|
||||
const addProvider = (provider: string) => {
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
if (!normalizedProvider) {
|
||||
return;
|
||||
}
|
||||
const parsed = parseModelRef(ref, DEFAULT_PROVIDER, {
|
||||
allowPluginNormalization: false,
|
||||
});
|
||||
if (!parsed) {
|
||||
continue;
|
||||
providers.add(normalizedProvider);
|
||||
};
|
||||
const configuredModels = params.cfg.agents?.defaults?.models;
|
||||
if (configuredModels) {
|
||||
for (const key of Object.keys(configuredModels)) {
|
||||
const ref = key.trim();
|
||||
if (!ref || !ref.includes("/")) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseModelRef(ref, DEFAULT_PROVIDER, {
|
||||
allowPluginNormalization: false,
|
||||
});
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
if (parsed.model === model || parsed.model.toLowerCase() === normalized) {
|
||||
addProvider(parsed.provider);
|
||||
if (providers.size > 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.model === model || parsed.model.toLowerCase() === normalized) {
|
||||
providers.add(parsed.provider);
|
||||
}
|
||||
const configuredProviders = params.cfg.models?.providers;
|
||||
if (configuredProviders) {
|
||||
for (const [providerId, providerConfig] of Object.entries(configuredProviders)) {
|
||||
const models = providerConfig?.models;
|
||||
if (!Array.isArray(models)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of models) {
|
||||
const modelId = entry?.id?.trim();
|
||||
if (!modelId) {
|
||||
continue;
|
||||
}
|
||||
if (modelId === model || modelId.toLowerCase() === normalized) {
|
||||
addProvider(providerId);
|
||||
}
|
||||
}
|
||||
if (providers.size > 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,16 @@ const listTasksForRelatedSessionKeyForOwnerMock = vi.hoisted(() =>
|
|||
[] as Array<Record<string, unknown>>,
|
||||
),
|
||||
);
|
||||
const resolveEnvApiKeyMock = vi.hoisted(
|
||||
() => vi.fn((_provider?: string, _env?: NodeJS.ProcessEnv) => null),
|
||||
);
|
||||
const resolveUsableCustomProviderApiKeyMock = vi.hoisted(
|
||||
() =>
|
||||
vi.fn(
|
||||
(_params?: { provider?: string }) =>
|
||||
null as { apiKey: string; source: string } | null,
|
||||
),
|
||||
);
|
||||
|
||||
const createMockConfig = () => ({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
|
|
@ -147,8 +157,8 @@ function createAuthProfilesModuleMock() {
|
|||
|
||||
function createModelAuthModuleMock() {
|
||||
return {
|
||||
resolveEnvApiKey: () => null,
|
||||
resolveUsableCustomProviderApiKey: () => null,
|
||||
resolveEnvApiKey: resolveEnvApiKeyMock,
|
||||
resolveUsableCustomProviderApiKey: resolveUsableCustomProviderApiKeyMock,
|
||||
resolveModelAuthMode: () => "api-key",
|
||||
};
|
||||
}
|
||||
|
|
@ -208,6 +218,10 @@ function resetSessionStore(store: Record<string, SessionEntry>) {
|
|||
buildStatusMessageMock.mockClear();
|
||||
resolveQueueSettingsMock.mockClear();
|
||||
resolveQueueSettingsMock.mockReturnValue({ mode: "interrupt" });
|
||||
resolveEnvApiKeyMock.mockReset();
|
||||
resolveEnvApiKeyMock.mockReturnValue(null);
|
||||
resolveUsableCustomProviderApiKeyMock.mockReset();
|
||||
resolveUsableCustomProviderApiKeyMock.mockReturnValue(null);
|
||||
loadSessionStoreMock.mockClear();
|
||||
updateSessionStoreMock.mockClear();
|
||||
callGatewayMock.mockClear();
|
||||
|
|
@ -512,6 +526,54 @@ describe("session_status tool", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("infers configured custom providers for runtime-only models in session_status", async () => {
|
||||
resetSessionStore({
|
||||
main: {
|
||||
sessionId: "runtime-custom-provider",
|
||||
updatedAt: 10,
|
||||
model: "qwen-max",
|
||||
},
|
||||
});
|
||||
mockConfig = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.4" },
|
||||
models: {},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"qwen-dashscope": {
|
||||
apiKey: "DASHSCOPE_API_KEY",
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
agentToAgent: { enabled: false },
|
||||
},
|
||||
};
|
||||
resolveUsableCustomProviderApiKeyMock.mockImplementation((params) =>
|
||||
params?.provider === "qwen-dashscope" ? { apiKey: "sk-test", source: "models.json" } : null,
|
||||
);
|
||||
|
||||
const tool = getSessionStatusTool();
|
||||
|
||||
await tool.execute("call-runtime-custom-provider", {});
|
||||
|
||||
expect(buildStatusMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agent: expect.objectContaining({
|
||||
model: expect.objectContaining({
|
||||
primary: "qwen-dashscope/qwen-max",
|
||||
}),
|
||||
}),
|
||||
modelAuth: "api-key (models.json)",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves an unknown runtime provider in the selected status card model", async () => {
|
||||
resetSessionStore({
|
||||
main: {
|
||||
|
|
|
|||
|
|
@ -726,6 +726,28 @@ describe("resolveSessionModelIdentityRef", () => {
|
|||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("infers provider from configured provider catalogs when allowlist is absent", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "google-gemini-cli/gemini-3-pro-preview",
|
||||
});
|
||||
cfg.models = {
|
||||
providers: {
|
||||
"qwen-dashscope": {
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig["models"];
|
||||
|
||||
const resolved = resolveSessionModelIdentityRef(cfg, {
|
||||
sessionId: "custom-provider-runtime-model",
|
||||
updatedAt: Date.now(),
|
||||
model: "qwen-max",
|
||||
modelProvider: undefined,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "qwen-dashscope", model: "qwen-max" });
|
||||
});
|
||||
|
||||
test("keeps provider unknown when configured models are ambiguous", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "google-gemini-cli/gemini-3-pro-preview",
|
||||
|
|
@ -740,6 +762,31 @@ describe("resolveSessionModelIdentityRef", () => {
|
|||
expect(resolved).toEqual({ model: "claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("keeps provider unknown when configured provider catalog matches are ambiguous", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "google-gemini-cli/gemini-3-pro-preview",
|
||||
});
|
||||
cfg.models = {
|
||||
providers: {
|
||||
"qwen-dashscope": {
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
modelstudio: {
|
||||
models: [{ id: "qwen-max" }],
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig["models"];
|
||||
|
||||
const resolved = resolveSessionModelIdentityRef(cfg, {
|
||||
sessionId: "ambiguous-custom-provider-runtime-model",
|
||||
updatedAt: Date.now(),
|
||||
model: "qwen-max",
|
||||
modelProvider: undefined,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ model: "qwen-max" });
|
||||
});
|
||||
|
||||
test("preserves provider from slash-prefixed runtime model", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "google-gemini-cli/gemini-3-pro-preview",
|
||||
|
|
|
|||
Loading…
Reference in New Issue