feat(ui-models): tag the provider rows this deployment declared
A row's stored profile could not tell a hand-declared gateway from a shipped provider whose models someone narrowed — both look identical from outside the adapter — so the Models page had no way to mark the routes a deployment added itself. The directory entry now carries `declared`, answered by the owning adapter against its own installed catalog, and the page renders a Custom tag from it. Absence stays "this adapter draws no such distinction" rather than "shipped", so a route no adapter claims is labelled neither way. Also records the default-route work's Agent Note and the e2e evidence for all three changes: the composer switch writing the section, and the Models page declaring a route with its own reasoning effort.
This commit is contained in:
@@ -26,7 +26,8 @@ import {
|
||||
// Type-only: brings the `ctx.tools` Context merge into this program (viewFor reads presenters).
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {
|
||||
ApiProxy, CredentialView, GoalRef, HistoryEntry, HostFrame, ModelCatalogFailure, ModelProviderGroup,
|
||||
ApiProxy, ConfigurableProviderView, CredentialView, GoalRef, HistoryEntry, HostFrame,
|
||||
ModelCatalogFailure, ModelProviderGroup,
|
||||
ModelReasoning, MuxFrame, QuestionResponsePayload, SessionProjectionsBlock, SessionSearchItem,
|
||||
QueuedInboxItem, SessionSummary, SettingsNamespaceView, SubagentAddress, ToolEventView,
|
||||
WorkspaceId, WorkspaceView,
|
||||
@@ -2558,15 +2559,17 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const active = new Set(registered.map(provider => provider.id))
|
||||
const directory = ctx.llm.listConfigurableProviders()
|
||||
const declared = new Set(directory.map(entry => entry.provider))
|
||||
const views = directory.map(entry => ({
|
||||
const views: ConfigurableProviderView[] = directory.map(entry => ({
|
||||
provider: entry.provider,
|
||||
displayName: entry.displayName,
|
||||
settingsNs: entry.settingsNs,
|
||||
settingsPath: [...entry.settingsPath],
|
||||
active: active.has(entry.provider),
|
||||
...entry.declared === undefined ? {} : { declared: entry.declared },
|
||||
}))
|
||||
// Routes registered without a directory declaration still appear —
|
||||
// they exist and serve models — just with no settings address.
|
||||
// they exist and serve models — just with no settings address. No
|
||||
// adapter claimed them, so nothing can say whether they are shipped.
|
||||
for (const provider of registered) {
|
||||
if (declared.has(provider.id)) continue
|
||||
views.push({
|
||||
|
||||
@@ -16,6 +16,7 @@ export const configurableProviderViewSchema = z.object({
|
||||
settingsNs: z.string(),
|
||||
settingsPath: z.array(z.string()),
|
||||
active: z.boolean(),
|
||||
declared: z.boolean().optional(),
|
||||
}) satisfies z.ZodType<Wire<ConfigurableProviderView>>
|
||||
|
||||
/** llm.providers request payload. */
|
||||
|
||||
@@ -22,6 +22,12 @@ export interface ConfigurableProviderView {
|
||||
settingsPath: string[]
|
||||
/** Whether the route is currently registered (its models are requestable). */
|
||||
active: boolean
|
||||
/**
|
||||
* Whether the owning adapter knows this route only because configuration
|
||||
* declared it. Absent when the adapter draws no such distinction, so a
|
||||
* surface must treat absence as "unknown", not as "shipped".
|
||||
*/
|
||||
declared?: boolean
|
||||
}
|
||||
|
||||
/** Llm-domain unary methods (the map keys llm.* of RpcMethodMap). */
|
||||
|
||||
@@ -66,22 +66,23 @@ export interface Config extends DefaultRouteSettings {
|
||||
workspaceRoot?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The default-route fields, as fresh schema instances. Both the plugin config
|
||||
* and the settings section are built from this one call, so the section stays
|
||||
* a subset of the config structurally rather than by a comment two people have
|
||||
* to keep true.
|
||||
*/
|
||||
function defaultRouteFields(): { [K in keyof Required<DefaultRouteSettings>]: z<string> } {
|
||||
return {
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
reasoningEffort: z.string(),
|
||||
}
|
||||
}
|
||||
/** The config fields the settings section carries; the rest stay launcher-owned. */
|
||||
const DEFAULT_ROUTE_FIELDS = ['provider', 'model', 'reasoningEffort'] as const
|
||||
|
||||
/** Schema of the settings section. */
|
||||
const DefaultRouteSchema: z<DefaultRouteSettings> = z.object(defaultRouteFields())
|
||||
/**
|
||||
* The settings section's schema, picked out of the plugin config rather than
|
||||
* restated. The config stays a plain literal because the configuration-catalog
|
||||
* generator reads it statically; picking from it is what keeps the section a
|
||||
* subset of it as both evolve.
|
||||
* @param config - the plugin config schema to pick from.
|
||||
* @returns the section schema over {@link DEFAULT_ROUTE_FIELDS}.
|
||||
*/
|
||||
function defaultRouteSchema(config: z<Config>): z<DefaultRouteSettings> {
|
||||
const fields = Object.fromEntries(
|
||||
DEFAULT_ROUTE_FIELDS.map(field => [field, config.dict?.[field]]),
|
||||
)
|
||||
return z.object(fields) as z<DefaultRouteSettings>
|
||||
}
|
||||
|
||||
/** Project the stored/composed section onto the agent-facing target shape. */
|
||||
function routeTarget(settings: DefaultRouteSettings): AgentLlmTarget {
|
||||
@@ -106,7 +107,9 @@ export class ApiProxyService extends Service implements ApiProxy {
|
||||
]
|
||||
|
||||
static Config: z<Config> = z.object({
|
||||
...defaultRouteFields(),
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
reasoningEffort: z.string(),
|
||||
workspaceRoot: z.string(),
|
||||
})
|
||||
|
||||
@@ -135,7 +138,7 @@ export class ApiProxyService extends Service implements ApiProxy {
|
||||
...config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort },
|
||||
}
|
||||
let route: () => DefaultRouteSettings = () => entry
|
||||
installSettingsSection(ctx, API_GATEWAY_SETTINGS_NAMESPACE, DefaultRouteSchema, entry, {
|
||||
installSettingsSection(ctx, API_GATEWAY_SETTINGS_NAMESPACE, defaultRouteSchema(ApiProxyService.Config), entry, {
|
||||
setSource: (current) => {
|
||||
route = current
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user