refactor(gateway): isolate model catalog outcomes

This commit is contained in:
Patrick Erichsen
2026-08-11 13:39:30 -07:00
parent f64a010f66
commit 8a3279d873
2 changed files with 66 additions and 42 deletions
@@ -0,0 +1,54 @@
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type { ModelAuthAvailabilityEvaluation } from "../../agents/model-auth-availability.js";
import type { ProviderCatalogOutcome } from "../../plugins/provider-catalog.types.js";
export function projectPublicProviderCatalogOutcomes(
outcomes: readonly ProviderCatalogOutcome[] | undefined,
): ProviderCatalogOutcome[] | undefined {
return outcomes?.map(({ provider, profileId, status }) => ({
provider,
...(profileId ? { profileId } : {}),
status,
}));
}
export function applyProviderCatalogOutcomesToModelAuth(params: {
provider: string;
modelId: string;
outcomes?: readonly ProviderCatalogOutcome[];
resolved: ModelAuthAvailabilityEvaluation;
evaluateForProfile: (profileId: string) => ModelAuthAvailabilityEvaluation;
}): ModelAuthAvailabilityEvaluation {
const provider = normalizeProviderId(params.provider);
const outcomes =
params.outcomes?.filter((outcome) => normalizeProviderId(outcome.provider) === provider) ?? [];
const modelId = params.modelId.trim().toLowerCase();
const authorizingProfileIds = outcomes.flatMap((outcome) =>
outcome.status === "ready" &&
outcome.profileId &&
outcome.modelIds?.some((candidate) => candidate.trim().toLowerCase() === modelId)
? [outcome.profileId]
: [],
);
if (
authorizingProfileIds.length > 0 &&
!authorizingProfileIds.includes(params.resolved.selectedProfileId ?? "")
) {
for (const profileId of authorizingProfileIds) {
const candidate = params.evaluateForProfile(profileId);
if (candidate.availability === true) {
return candidate;
}
}
return { ...params.resolved, availability: false };
}
// Stored credentials prove presence, not acceptance. Apply rejection only to
// the profile discovery tested, or another valid profile would be hidden.
return outcomes.some(
(outcome) =>
outcome.status === "auth-rejected" &&
(outcome.profileId === undefined || outcome.profileId === params.resolved.selectedProfileId),
)
? { ...params.resolved, availability: false }
: params.resolved;
}
@@ -58,6 +58,10 @@ import { normalizeAgentId } from "../../routing/session-key.js";
import type { GatewayAgentRuntime } from "../../shared/session-types.js";
import { resolveGatewayModelThinkingProfile } from "../session-utils-model.js";
import { createModelsListAuthResolver } from "./models-list-auth-resolver.js";
import {
applyProviderCatalogOutcomesToModelAuth,
projectPublicProviderCatalogOutcomes,
} from "./models-list-provider-outcomes.js";
import type { GatewayRequestContext } from "./types.js";
type ModelsListEntry = Pick<
@@ -76,14 +80,6 @@ type ModelsListResult = {
providerOutcomes?: readonly ProviderCatalogOutcome[];
};
function projectProviderCatalogOutcomes(outcomes: readonly ProviderCatalogOutcome[] | undefined) {
return outcomes?.map(({ provider, profileId, status }) => ({
provider,
...(profileId ? { profileId } : {}),
status,
}));
}
let loggedSlowModelsListCatalog = false;
// Unknown views are rejected by protocol validation first; this helper keeps the
@@ -210,39 +206,13 @@ function createModelsListEntryEvaluator(params: {
}),
}
: evaluation;
const provider = normalizeProviderId(entry.provider);
const outcomes =
params.providerOutcomes?.filter(
(outcome) => normalizeProviderId(outcome.provider) === provider,
) ?? [];
const modelId = (identity?.id ?? entry.id).trim().toLowerCase();
const authorizingProfiles = outcomes.filter(
(outcome) =>
outcome.status === "ready" &&
outcome.profileId !== undefined &&
outcome.modelIds?.some((candidate) => candidate.trim().toLowerCase() === modelId),
);
if (
authorizingProfiles.length > 0 &&
!authorizingProfiles.some((outcome) => outcome.profileId === resolved.selectedProfileId)
) {
for (const outcome of authorizingProfiles) {
const candidate = evaluateForProfile(outcome.profileId);
if (candidate.availability === true) {
return candidate;
}
}
return { ...resolved, availability: false };
}
// Stored credentials prove presence, not acceptance. Apply the live rejection only to the
// profile discovery tested; widening it would hide routes backed by another valid profile.
return outcomes.some(
(outcome) =>
outcome.status === "auth-rejected" &&
(outcome.profileId === undefined || outcome.profileId === resolved.selectedProfileId),
)
? { ...resolved, availability: false }
: resolved;
return applyProviderCatalogOutcomesToModelAuth({
provider: entry.provider,
modelId: identity?.id ?? entry.id,
outcomes: params.providerOutcomes,
resolved,
evaluateForProfile,
});
});
pending.set(cacheKey, next);
return next;
@@ -649,7 +619,7 @@ export async function buildModelsListResult(
const catalog = snapshot.entries;
const routeVariants = snapshot.routeVariants;
const providerOutcomes = snapshot.providerOutcomes;
const publicProviderOutcomes = projectProviderCatalogOutcomes(providerOutcomes);
const publicProviderOutcomes = projectPublicProviderCatalogOutcomes(providerOutcomes);
const outcomeProjection = publicProviderOutcomes?.length
? { providerOutcomes: publicProviderOutcomes }
: {};