kimi-code / packages /node-sdk /src /catalog.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw
History Blame Contribute Delete
5.55 kB
import type { KimiConfig, ModelAlias } from '#/config/index';
import {
catalogBaseUrl,
catalogProviderModels,
inferWireType,
resolveCatalogImport,
type Catalog,
type CatalogImportInvalidReason,
type CatalogImportResolution,
type CatalogModel,
type CatalogProviderEntry,
type ModelCapability,
type ProviderType,
} from '@moonshot-ai/kosong';
export { catalogBaseUrl, catalogProviderModels, inferWireType, resolveCatalogImport };
export type { CatalogImportInvalidReason, CatalogImportResolution };
export type { Catalog, CatalogModel, CatalogProviderEntry };
export const DEFAULT_CATALOG_URL = 'https://models.dev/api.json';
export class CatalogFetchError extends Error {
readonly status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
export interface FetchCatalogOptions {
readonly signal?: AbortSignal;
readonly fetchImpl?: typeof fetch;
readonly userAgent?: string;
}
/**
* Fetches a models.dev-style catalog. Public endpoint, no credentials needed.
* `userAgent` identifies the host product (e.g. `kimi-code-cli/1.2.3`); when
* omitted the request falls back to the runtime default (`User-Agent: node`).
*/
export async function fetchCatalog(
url: string,
options: FetchCatalogOptions = {},
): Promise<Catalog> {
const { signal, fetchImpl = fetch, userAgent } = options;
const headers: Record<string, string> = { Accept: 'application/json' };
if (userAgent !== undefined) headers['User-Agent'] = userAgent;
const res = await fetchImpl(url, { headers, signal });
if (!res.ok) {
throw new CatalogFetchError(`Failed to fetch catalog (HTTP ${res.status}).`, res.status);
}
const payload: unknown = await res.json();
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
throw new Error(`Unexpected catalog response from ${url}.`);
}
return payload as Catalog;
}
function capabilityToStrings(capability: ModelCapability): string[] | undefined {
const caps: string[] = [];
if (capability.image_in) caps.push('image_in');
if (capability.video_in) caps.push('video_in');
if (capability.audio_in) caps.push('audio_in');
if (capability.thinking) caps.push('thinking');
if (capability.tool_use) caps.push('tool_use');
if (capability.dynamically_loaded_tools === true) caps.push('dynamically_loaded_tools');
return caps.length > 0 ? caps : undefined;
}
/** Builds a kimi-code model alias from a normalized catalog model. */
export function catalogModelToAlias(providerId: string, model: CatalogModel): ModelAlias {
const caps = capabilityToStrings(model.capability);
return {
provider: providerId,
model: model.id,
maxContextSize: model.capability.max_context_tokens,
maxInputSize: model.capability.max_input_tokens,
maxOutputSize: model.maxOutputSize,
// A model that always reasons advertises `always_thinking` instead of
// `thinking`, so the UI locks thinking on and offers no off option.
capabilities:
model.alwaysThinking === true
? caps?.map((cap) => (cap === 'thinking' ? 'always_thinking' : cap))
: caps,
displayName: model.name,
reasoningKey: model.reasoningKey,
supportEfforts: model.supportEfforts === undefined ? undefined : [...model.supportEfforts],
offEffort: model.offEffort,
protocol: model.protocol,
baseUrl: model.baseUrl,
};
}
export interface ApplyCatalogProviderOptions {
readonly providerId: string;
readonly wire: ProviderType;
readonly baseUrl?: string;
readonly apiKey: string;
readonly models: readonly CatalogModel[];
readonly selectedModelId: string;
readonly thinking: boolean;
}
/**
* Parses an optional pruned models.dev catalog string — typically the
* `__KIMI_CODE_BUILT_IN_CATALOG__` constant injected by tsdown at build
* time. Returns `undefined` when the argument is missing or invalid.
*/
export function loadBuiltInCatalog(text?: string): Catalog | undefined {
if (typeof text !== 'string' || text.length === 0) return undefined;
try {
return JSON.parse(text) as Catalog;
} catch {
return undefined;
}
}
/**
* Writes a catalog-selected provider and its model aliases into `config` and
* marks it the default. Model metadata (context, output limit, capabilities)
* comes from the catalog, so the user does not hand-write it. Returns the
* default model key.
*
* NOTE: the same-provider cleanup below mutates the passed-in `config` only.
* It clears stale aliases on disk solely when the caller overwrites the whole
* config. Callers persisting via `setConfig` — a deep-merge patch that cannot
* delete keys — must call `removeProvider` first, or removed aliases reappear
* after the merge.
*/
export function applyCatalogProvider(
config: KimiConfig,
options: ApplyCatalogProviderOptions,
): { defaultModel: string } {
config.providers[options.providerId] = {
type: options.wire,
baseUrl: options.baseUrl,
apiKey: options.apiKey,
};
const models = config.models ?? {};
for (const [key, alias] of Object.entries(models)) {
if (alias.provider === options.providerId) delete models[key];
}
for (const model of options.models) {
models[`${options.providerId}/${model.id}`] = catalogModelToAlias(options.providerId, model);
}
config.models = models;
const defaultModel = `${options.providerId}/${options.selectedModelId}`;
config.defaultModel = defaultModel;
config.thinking = { ...config.thinking, enabled: options.thinking };
return { defaultModel };
}