The shared DeepSeek listing decoder reads an optional per-entry
`badge: { text, tone }`. Text is one literal or translations keyed by
lowercase language id with a required English entry, each trimmed to 1
to 12 characters on one line; tone is neutral, info, success, or
warning, and an absent or unrecognized tone reads as neutral. An absent
or null badge declares none; an unreadable one is dropped with a listing
problem and never hides its model.
resolveListedCatalog keeps the badges of offered models, both DeepSeek
routes attach them through catalogModelInfo, LlmRuntime.listModels
detaches them, and the Host model catalog forwards them as
ModelCatalogModel.badge. resolveModelInfo omits the badge, so no model
request, Session event, or model-facing tool carries it.
118 lines
4.8 KiB
TypeScript
118 lines
4.8 KiB
TypeScript
/** Shared projection of the live LLM registry into the browser model catalog. */
|
|
|
|
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
import type {} from '@deepseek-ai/dsh-settings'
|
|
import type { LlmModelInfo } from '@deepseek-ai/dsh-llm'
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type {
|
|
ModelCatalog,
|
|
ModelReasoning,
|
|
ModelSelection,
|
|
} from './types.ts'
|
|
|
|
/**
|
|
* Build the browser model catalog without requiring a Session.
|
|
* @param ctx - Host context carrying the live LLM registry.
|
|
* @param defaultSelection - deployment default used before a Session selects a model.
|
|
* @returns successful non-empty provider groups and isolated provider failures.
|
|
*/
|
|
export async function buildModelCatalog(
|
|
ctx: Context,
|
|
defaultSelection: ModelSelection = ctx.agentDefaultModel.currentSelection(),
|
|
): Promise<ModelCatalog> {
|
|
const providers = ctx.llm.listProviders()
|
|
const catalog = await Promise.all(providers.map(async (provider) => {
|
|
try {
|
|
const models = await ctx.llm.listModels(provider.id)
|
|
const entries = await Promise.all(models.map(async (model) => {
|
|
const resolved = await ctx.llm.resolveModelInfo(provider.id, model.id)
|
|
const reasoning: ModelReasoning | undefined = resolved.reasoning === undefined
|
|
? undefined
|
|
: {
|
|
efforts: resolved.reasoning.efforts.map(effort => ({
|
|
id: effort.id,
|
|
name: effort.name,
|
|
...(effort.description === undefined ? {} : { description: effort.description }),
|
|
})),
|
|
...(resolved.reasoning.defaultEffort === undefined
|
|
? {}
|
|
: { defaultEffort: resolved.reasoning.defaultEffort }),
|
|
}
|
|
return {
|
|
id: model.id,
|
|
name: model.name,
|
|
...(model.description === undefined ? {} : { description: model.description }),
|
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
...(model.badge === undefined ? {} : { badge: model.badge }),
|
|
}
|
|
}))
|
|
return {
|
|
kind: 'group' as const,
|
|
group: { id: provider.id, name: provider.name, models: entries },
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
kind: 'failure' as const,
|
|
failure: {
|
|
id: provider.id,
|
|
name: provider.name,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
},
|
|
}
|
|
}
|
|
}))
|
|
const groups = catalog.flatMap(item => item.kind === 'group' ? [item.group] : [])
|
|
.filter(group => group.models.length > 0)
|
|
return {
|
|
default: { ...defaultSelection },
|
|
routableProviders: groups.map(group => group.id),
|
|
groups,
|
|
failures: catalog.flatMap(item => item.kind === 'failure' ? [item.failure] : []),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a GUI selection against the current available provider catalog.
|
|
* @param ctx - Host LLM registry.
|
|
* @param selection - stored or explicitly requested selection.
|
|
* @returns whether the exact model is currently advertised as available.
|
|
*/
|
|
export async function modelAvailable(ctx: Context, selection: ModelSelection): Promise<boolean> {
|
|
if (!ctx.llm.listProviders().some(provider => provider.id === selection.provider)) return false
|
|
let models: readonly LlmModelInfo[]
|
|
try { models = await ctx.llm.listModels(selection.provider) }
|
|
catch (error) {
|
|
throw new RemoteError('session/model-unavailable',
|
|
error instanceof Error ? error.message : String(error),
|
|
{ provider: selection.provider, model: selection.model })
|
|
}
|
|
return models.some(model => model.id === selection.model)
|
|
}
|
|
|
|
/**
|
|
* Check configured provider API-key references independently of model availability.
|
|
* @param ctx - Host registry, settings, and credential services.
|
|
* @returns whether any API-key provider has a configured credential.
|
|
*/
|
|
export async function hasProviderApiKey(ctx: Context): Promise<boolean> {
|
|
const settings = ctx.get('settings')
|
|
const credentials = ctx.get('credentials')
|
|
if (settings === undefined || credentials === undefined) {
|
|
throw new RemoteError('session/provider-credentials-unavailable', 'provider credentials are unavailable', {})
|
|
}
|
|
const namespaces = settings.describe({ redactSecrets: true })
|
|
for (const provider of ctx.llm.listConfigurableProviders()) {
|
|
if (provider.provider === 'deepseek-account') continue
|
|
let profile = namespaces.find(namespace => namespace.ns === provider.settingsNs)?.value
|
|
for (const key of provider.settingsPath) {
|
|
profile = typeof profile === 'object' && profile !== null ? Reflect.get(profile, key) : undefined
|
|
}
|
|
if (typeof profile !== 'object' || profile === null) continue
|
|
const ref: unknown = Reflect.get(profile, 'apiKeyEnv')
|
|
if (typeof ref === 'string' && ref.length > 0
|
|
&& (await credentials.describe(credentialRef(ref))).configured) return true
|
|
}
|
|
return false
|
|
}
|