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:
Yichen Jiang
2026-08-07 13:49:47 +08:00
parent e0f9f7a6e6
commit 9679597204
38 changed files with 479 additions and 56 deletions

View File

@@ -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
},