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:
狼哥 2026-04-01 09:09:42 +08:00 committed by GitHub
parent 547154865b
commit 40b24dfa6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 198 additions and 21 deletions

View File

@ -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", () => {

View File

@ -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;
}

View File

@ -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: {

View File

@ -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",