mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-21 19:52:47 +02:00
Add more data to the models catalog list
This commit is contained in:
@@ -44,6 +44,20 @@ export function getCatalogProviderForType(
|
||||
return PROVIDER_CATALOG_MAP[type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-model feature flags as reported upstream. `null` means the catalog has
|
||||
* no data for that model - deliberately distinct from `false`, so consumers
|
||||
* can tell "unsupported" apart from "unknown".
|
||||
*/
|
||||
export type AiModelCapabilityFlags = {
|
||||
functionCalling: boolean | null;
|
||||
vision: boolean | null;
|
||||
promptCaching: boolean | null;
|
||||
reasoning: boolean | null;
|
||||
responseSchema: boolean | null;
|
||||
webSearch: boolean | null;
|
||||
};
|
||||
|
||||
export type AiModelCatalogEntry = {
|
||||
provider: CatalogProvider;
|
||||
model: string;
|
||||
@@ -53,8 +67,20 @@ export type AiModelCatalogEntry = {
|
||||
cache: number | null;
|
||||
reasoning: number | null;
|
||||
};
|
||||
limits: {
|
||||
/** Context window. */
|
||||
input: number | null;
|
||||
/** Cap on the output/max_tokens request parameter. */
|
||||
output: number | null;
|
||||
};
|
||||
capabilities: AiModelCapabilityFlags;
|
||||
};
|
||||
|
||||
const flag = z.boolean().nullable().optional();
|
||||
|
||||
// limits/capabilities are optional so a catalog published before they were
|
||||
// added (or an operator's own merge_file) still parses - those entries just
|
||||
// report unknown metadata rather than failing the whole payload.
|
||||
const catalogEntrySchema = z.object({
|
||||
model: z.string(),
|
||||
provider: z.string(),
|
||||
@@ -65,6 +91,22 @@ const catalogEntrySchema = z.object({
|
||||
cache: z.number().nullable().optional(),
|
||||
reasoning: z.number().nullable().optional()
|
||||
})
|
||||
.optional(),
|
||||
limits: z
|
||||
.object({
|
||||
input: z.number().nullable().optional(),
|
||||
output: z.number().nullable().optional()
|
||||
})
|
||||
.optional(),
|
||||
capabilities: z
|
||||
.object({
|
||||
functionCalling: flag,
|
||||
vision: flag,
|
||||
promptCaching: flag,
|
||||
reasoning: flag,
|
||||
responseSchema: flag,
|
||||
webSearch: flag
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
@@ -108,6 +150,18 @@ function normalizeEntry(raw: RawCatalogEntry): AiModelCatalogEntry | null {
|
||||
out: raw.pricing?.out ?? null,
|
||||
cache: raw.pricing?.cache ?? null,
|
||||
reasoning: raw.pricing?.reasoning ?? null
|
||||
},
|
||||
limits: {
|
||||
input: raw.limits?.input ?? null,
|
||||
output: raw.limits?.output ?? null
|
||||
},
|
||||
capabilities: {
|
||||
functionCalling: raw.capabilities?.functionCalling ?? null,
|
||||
vision: raw.capabilities?.vision ?? null,
|
||||
promptCaching: raw.capabilities?.promptCaching ?? null,
|
||||
reasoning: raw.capabilities?.reasoning ?? null,
|
||||
responseSchema: raw.capabilities?.responseSchema ?? null,
|
||||
webSearch: raw.capabilities?.webSearch ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -284,34 +338,44 @@ export class AiModelCatalog {
|
||||
|
||||
export const aiModelCatalog = new AiModelCatalog();
|
||||
|
||||
/**
|
||||
* Full catalog entries for a provider type, deduplicated by model id and
|
||||
* sorted by id. Model discovery uses these to report real token limits and
|
||||
* capability flags; `listCatalogModelsForType` is the id-only view of the
|
||||
* same list.
|
||||
*/
|
||||
export function listCatalogEntriesForType(
|
||||
type: AiProviderType,
|
||||
query?: string
|
||||
): AiModelCatalogEntry[] {
|
||||
const catalogProvider = getCatalogProviderForType(type);
|
||||
|
||||
let entries = catalogProvider ? aiModelCatalog.list(catalogProvider) : [];
|
||||
|
||||
if (query) {
|
||||
const q = query.toLowerCase();
|
||||
entries = entries.filter((e) => e.model.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
entries = entries.filter((e) => {
|
||||
if (seen.has(e.model)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(e.model);
|
||||
return true;
|
||||
});
|
||||
|
||||
return [...entries].sort((a, b) => a.model.localeCompare(b.model));
|
||||
}
|
||||
|
||||
export function listCatalogModelsForType(
|
||||
type: AiProviderType,
|
||||
query?: string
|
||||
): { model: string }[] {
|
||||
const catalogProvider = getCatalogProviderForType(type);
|
||||
|
||||
let models = catalogProvider
|
||||
? aiModelCatalog.list(catalogProvider).map((entry) => ({
|
||||
model: entry.model
|
||||
}))
|
||||
: [];
|
||||
|
||||
if (query) {
|
||||
const q = query.toLowerCase();
|
||||
models = models.filter((m) => m.model.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
models = models.filter((m) => {
|
||||
if (seen.has(m.model)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(m.model);
|
||||
return true;
|
||||
});
|
||||
|
||||
models.sort((a, b) => a.model.localeCompare(b.model));
|
||||
return models;
|
||||
return listCatalogEntriesForType(type, query).map((entry) => ({
|
||||
model: entry.model
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
isAllowedByLists,
|
||||
isModelKeyPattern
|
||||
} from "@server/lib/aiModelKeyMatch";
|
||||
import type { AiModelCapabilityFlags } from "@server/lib/aiModelCatalog";
|
||||
|
||||
// Anthropic's Models API pagination: 20 per page by default, 1..1000.
|
||||
export const MODEL_PAGE_DEFAULT_LIMIT = 20;
|
||||
@@ -25,12 +26,66 @@ export type AnthropicModelInfo = {
|
||||
created_at: string;
|
||||
max_input_tokens: number | null;
|
||||
max_tokens: number | null;
|
||||
capabilities: null;
|
||||
capabilities: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
/** A model row an administrator configured explicitly on a provider. */
|
||||
export type ConfiguredModel = { name: string; createdAt: number };
|
||||
|
||||
/** What the pricing catalog knows about a model beyond its id. */
|
||||
export type CatalogModelMetadata = {
|
||||
maxInputTokens: number | null;
|
||||
maxOutputTokens: number | null;
|
||||
capabilities: AiModelCapabilityFlags;
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates the catalog's flat feature flags into the nested shape
|
||||
* Anthropic's Models API uses. Best-effort by nature: the catalog carries a
|
||||
* coarser set of flags than the Models API describes, so anything it reports
|
||||
* as unknown (`null`) is surfaced as unsupported rather than invented.
|
||||
*/
|
||||
export function capabilitiesFromCatalog(
|
||||
flags: AiModelCapabilityFlags
|
||||
): Record<string, unknown> {
|
||||
const supported = (value: boolean | null) => ({
|
||||
supported: value === true
|
||||
});
|
||||
// The catalog has a single `reasoning` flag and no way to distinguish
|
||||
// adaptive from budget_tokens-style thinking, so both variants follow it.
|
||||
const reasoning = flags.reasoning === true;
|
||||
|
||||
return {
|
||||
batch: supported(null),
|
||||
citations: supported(null),
|
||||
code_execution: supported(null),
|
||||
context_management: {
|
||||
supported: false,
|
||||
clear_thinking_20251015: null,
|
||||
clear_tool_uses_20250919: null,
|
||||
compact_20260112: null
|
||||
},
|
||||
effort: {
|
||||
supported: reasoning,
|
||||
low: supported(flags.reasoning),
|
||||
medium: supported(flags.reasoning),
|
||||
high: supported(flags.reasoning),
|
||||
max: supported(flags.reasoning),
|
||||
xhigh: null
|
||||
},
|
||||
image_input: supported(flags.vision),
|
||||
pdf_input: supported(null),
|
||||
structured_outputs: supported(flags.responseSchema),
|
||||
thinking: {
|
||||
supported: reasoning,
|
||||
types: {
|
||||
adaptive: { supported: reasoning },
|
||||
enabled: { supported: reasoning }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One attached provider's contribution to a resource's model listing, with the
|
||||
* allow/block lists already resolved for the attachment's access mode.
|
||||
@@ -40,12 +95,13 @@ export type ModelDiscoveryProvider = {
|
||||
allows: string[];
|
||||
blocks: string[];
|
||||
/**
|
||||
* Concrete model ids the provider's type is known to serve. This is what
|
||||
* lets a wildcard allow such as `claude-*` enumerate into real ids;
|
||||
* provider types with no catalog (aggregators, custom) pass an empty list
|
||||
* and surface only their exact allow entries.
|
||||
* Concrete model ids the provider's type is known to serve, with whatever
|
||||
* the catalog knows about each. This is what lets a wildcard allow such as
|
||||
* `claude-*` enumerate into real ids; provider types with no catalog
|
||||
* (aggregators, custom) pass an empty map and surface only their exact
|
||||
* allow entries.
|
||||
*/
|
||||
catalogModelIds: string[];
|
||||
catalog: Map<string, CatalogModelMetadata>;
|
||||
/** Keyed by model key, for display names and creation times. */
|
||||
configured: Map<string, ConfiguredModel>;
|
||||
};
|
||||
@@ -73,7 +129,7 @@ export function expandProviderModels(
|
||||
candidates.add(allow);
|
||||
}
|
||||
}
|
||||
for (const modelId of provider.catalogModelIds) {
|
||||
for (const modelId of provider.catalog.keys()) {
|
||||
candidates.add(modelId);
|
||||
}
|
||||
|
||||
@@ -83,6 +139,8 @@ export function expandProviderModels(
|
||||
continue;
|
||||
}
|
||||
const configured = provider.configured.get(modelKey);
|
||||
const catalog = provider.catalog.get(modelKey);
|
||||
|
||||
models.push({
|
||||
type: "model",
|
||||
id: modelKey,
|
||||
@@ -90,9 +148,11 @@ export function expandProviderModels(
|
||||
created_at: configured
|
||||
? new Date(configured.createdAt).toISOString()
|
||||
: UNKNOWN_CREATED_AT,
|
||||
max_input_tokens: null,
|
||||
max_tokens: null,
|
||||
capabilities: null
|
||||
max_input_tokens: catalog?.maxInputTokens ?? null,
|
||||
max_tokens: catalog?.maxOutputTokens ?? null,
|
||||
capabilities: catalog
|
||||
? capabilitiesFromCatalog(catalog.capabilities)
|
||||
: null
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { logsDb, db, orgs, aiSessionLog, type AiProvider } from "@server/db";
|
||||
import type { InferInsertModel } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import cache from "#private/lib/cache";
|
||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||
import { sanitizeString } from "@server/lib/sanitize";
|
||||
import type { AiCapability } from "@server/lib/aiCapabilities";
|
||||
|
||||
@@ -15,12 +15,13 @@ import {
|
||||
isAiGatewayTrustHeaderValid
|
||||
} from "@server/lib/aiGatewayTrust";
|
||||
import { resolveEffectiveLists } from "@server/lib/aiInferenceResource";
|
||||
import { listCatalogModelsForType } from "@server/lib/aiModelCatalog";
|
||||
import { listCatalogEntriesForType } from "@server/lib/aiModelCatalog";
|
||||
import {
|
||||
listPermittedModels,
|
||||
paginateModels,
|
||||
MODEL_PAGE_DEFAULT_LIMIT,
|
||||
MODEL_PAGE_MAX_LIMIT,
|
||||
type CatalogModelMetadata,
|
||||
type ConfiguredModel,
|
||||
type ModelDiscoveryProvider
|
||||
} from "@server/lib/aiModelDiscovery";
|
||||
@@ -113,6 +114,20 @@ async function loadProviderModelLists(
|
||||
return lists;
|
||||
}
|
||||
|
||||
function catalogMetadataForType(
|
||||
type: AiProviderType
|
||||
): Map<string, CatalogModelMetadata> {
|
||||
const metadata = new Map<string, CatalogModelMetadata>();
|
||||
for (const entry of listCatalogEntriesForType(type)) {
|
||||
metadata.set(entry.model, {
|
||||
maxInputTokens: entry.limits.input,
|
||||
maxOutputTokens: entry.limits.output,
|
||||
capabilities: entry.capabilities
|
||||
});
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function buildDiscoveryProviders(
|
||||
attachments: ProviderAttachment[],
|
||||
resourceListsByProvider: Map<number, ProviderPatternLists>,
|
||||
@@ -133,9 +148,9 @@ function buildDiscoveryProviders(
|
||||
providerId,
|
||||
allows,
|
||||
blocks,
|
||||
catalogModelIds: listCatalogModelsForType(
|
||||
catalog: catalogMetadataForType(
|
||||
attachment.provider.type as AiProviderType
|
||||
).map((entry) => entry.model),
|
||||
),
|
||||
configured: lists.configuredByProvider.get(providerId) ?? new Map()
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user