From cddfcfde143a96c136cacd39d372ecdc7f5ca292 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Thu, 16 Jul 2026 15:38:37 +0800 Subject: [PATCH 1/4] fix: scope Anthropic effort fallback profile to non-Kimi providers Managed Kimi models routed through the Anthropic protocol (protocol = "anthropic", no catalog-declared think_efforts) inherited the inferred latest-Opus effort profile, so the UI showed reasoning effort choices the server never declared. The fallback now applies only when the provider type is known, non-kimi, and the effective wire protocol is Anthropic; Kimi providers keep catalog-declared efforts only, and callers without provider context fall back to name matching. --- .../fix-kimi-provider-effort-fallback.md | 5 ++ apps/kimi-code/src/tui/commands/config.ts | 2 +- .../test/tui/kimi-tui-message-flow.test.ts | 28 ++++++++- packages/acp-adapter/src/model-catalog.ts | 54 ++++++++--------- .../acp-adapter/test/config-options.test.ts | 30 +++++++++- .../acp-adapter/test/model-catalog.test.ts | 31 ++++++++++ .../agent-core-v2/src/app/model/modelAuth.ts | 19 +++--- .../src/app/model/modelResolverService.ts | 8 ++- .../src/app/modelCatalog/modelCatalog.ts | 6 +- .../app/modelCatalog/modelCatalogService.ts | 9 +-- .../test/app/model/model.test.ts | 59 ++++++++++++++----- .../test/app/model/modelResolver.test.ts | 6 +- .../app/modelCatalog/modelCatalog.test.ts | 26 +++++++- packages/agent-core/src/config/model.ts | 24 ++++---- .../src/services/modelCatalog/modelCatalog.ts | 6 +- .../modelCatalog/modelCatalogService.ts | 10 ++-- .../src/session/provider-manager.ts | 3 +- .../test/config/model-overrides.test.ts | 32 ++++++++-- .../services/model-catalog-service.test.ts | 25 +++++++- 19 files changed, 290 insertions(+), 93 deletions(-) create mode 100644 .changeset/fix-kimi-provider-effort-fallback.md diff --git a/.changeset/fix-kimi-provider-effort-fallback.md b/.changeset/fix-kimi-provider-effort-fallback.md new file mode 100644 index 0000000000..9d74e71322 --- /dev/null +++ b/.changeset/fix-kimi-provider-effort-fallback.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix Kimi-provider models routed through the Anthropic protocol incorrectly showing reasoning effort options. Effort choices now come only from the model's declared metadata, and the inferred fallback profile applies solely to non-Kimi Anthropic-compatible providers. diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index e3d6ad852c..807dfa671c 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -47,7 +47,7 @@ function currentTuiConfig(host: SlashCommandHost): TuiConfig { function effectiveModelForHost(host: SlashCommandHost, model: ModelAlias): ModelAlias { const providerType = host.state.appState.availableProviders[model.provider]?.type; - return effectiveModelAlias(model, (model.protocol ?? providerType) === 'anthropic'); + return effectiveModelAlias(model, providerType); } export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise { diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 028738cac2..6e0a6b8b25 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -5202,6 +5202,32 @@ describe('/effort support_efforts override', () => { }); it('offers the latest Opus efforts for an unknown Anthropic-compatible model', async () => { + const { driver } = await makeDriver(makeSession(), { + getConfig: vi.fn(async () => ({ + providers: { + compatible: { type: 'anthropic', apiKey: 'test-key' }, + }, + models: { + k2: { + provider: 'compatible', + model: 'compatible-model', + maxContextSize: 100, + }, + }, + defaultModel: 'k2', + })), + }); + + driver.handleUserInput('/effort'); + + await vi.waitFor(() => { + expect(driver.state.editorContainer.children[0]).toBeInstanceOf(EffortSelectorComponent); + }); + const picker = driver.state.editorContainer.children[0] as EffortSelectorComponent; + expect(picker.render(80).join('\n')).toContain('Max'); + }); + + it('offers no fallback efforts for an unknown model on a Kimi provider using the Anthropic protocol', async () => { const { driver } = await makeDriver(makeSession(), { getConfig: vi.fn(async () => ({ providers: { @@ -5225,7 +5251,7 @@ describe('/effort support_efforts override', () => { expect(driver.state.editorContainer.children[0]).toBeInstanceOf(EffortSelectorComponent); }); const picker = driver.state.editorContainer.children[0] as EffortSelectorComponent; - expect(picker.render(80).join('\n')).toContain('Max'); + expect(picker.render(80).join('\n')).not.toContain('Max'); }); it('keeps rejecting efforts hidden by a Kimi support_efforts override', async () => { diff --git a/packages/acp-adapter/src/model-catalog.ts b/packages/acp-adapter/src/model-catalog.ts index 05e5a4e21e..dbf80bc61d 100644 --- a/packages/acp-adapter/src/model-catalog.ts +++ b/packages/acp-adapter/src/model-catalog.ts @@ -17,7 +17,7 @@ * `thinkingSupported` is true if any of: * 1. the alias's declared `capabilities` array contains `'thinking'` * (including the capability inferred from the Anthropic wire protocol — - * see the `anthropicCompatible` context below), or + * see the `providerType` context below), or * 2. the underlying model name matches `/thinking|reason/i` * (always-thinking variants), or * 3. the underlying model name is on the {@link TOGGLEABLE_THINKING_MODELS} @@ -26,12 +26,12 @@ * The runtime resolves a model's wire protocol from * `alias.protocol ?? provider.type` (see * `ProviderManager.resolveProviderConfig`). The derive helpers below take the - * provider-derived `anthropicCompatible` flag as an optional second argument - * so the catalog agrees with the runtime about Anthropic profiles even when - * the alias itself does not declare `protocol`. + * provider's `type` as an optional second argument so the catalog agrees with + * the runtime about Anthropic profiles even when the alias itself does not + * declare `protocol`. */ -import { effectiveModelAlias } from '@moonshot-ai/agent-core'; +import { effectiveModelAlias, type ProviderType } from '@moonshot-ai/agent-core'; import type { KimiHarness, ModelAlias } from '@moonshot-ai/kimi-code-sdk'; /** @@ -64,8 +64,8 @@ export interface AcpModelEntry { */ const TOGGLEABLE_THINKING_MODELS = new Set(['kimi-for-coding', 'kimi-code']); -export function deriveThinkingSupported(alias: ModelAlias, anthropicCompatible = false): boolean { - const effective = effectiveModelAlias(alias, anthropicCompatible); +export function deriveThinkingSupported(alias: ModelAlias, providerType?: ProviderType): boolean { + const effective = effectiveModelAlias(alias, providerType); const declared = effective.capabilities ?? []; if (declared.includes('thinking') || declared.includes('always_thinking')) return true; const lower = effective.model.toLowerCase(); @@ -81,8 +81,8 @@ export function deriveThinkingSupported(alias: ModelAlias, anthropicCompatible = * `thinkingSupported`, but only an explicit (server-derived) declaration * may remove the off option from the client. */ -export function deriveAlwaysThinking(alias: ModelAlias, anthropicCompatible = false): boolean { - return (effectiveModelAlias(alias, anthropicCompatible).capabilities ?? []).includes( +export function deriveAlwaysThinking(alias: ModelAlias, providerType?: ProviderType): boolean { + return (effectiveModelAlias(alias, providerType).capabilities ?? []).includes( 'always_thinking', ); } @@ -94,9 +94,9 @@ export function deriveAlwaysThinking(alias: ModelAlias, anthropicCompatible = fa */ export function deriveDefaultThinkingEffort( alias: ModelAlias, - anthropicCompatible = false, + providerType?: ProviderType, ): string { - const effective = effectiveModelAlias(alias, anthropicCompatible); + const effective = effectiveModelAlias(alias, providerType); const efforts = effective.supportEfforts; if (efforts !== undefined && efforts.length > 0) { return effective.defaultEffort ?? efforts[Math.floor(efforts.length / 2)]!; @@ -126,35 +126,35 @@ export async function listModelsFromHarness( if (models === undefined) return []; const out: AcpModelEntry[] = []; for (const [id, alias] of Object.entries(models)) { - const anthropicCompatible = usesAnthropicProvider(alias, config); - const effective = effectiveModelAlias(alias, anthropicCompatible); + const providerType = providerTypeOf(alias, config); + const effective = effectiveModelAlias(alias, providerType); out.push({ id, name: effective.displayName ?? effective.model ?? id, - thinkingSupported: deriveThinkingSupported(alias, anthropicCompatible), - alwaysThinking: deriveAlwaysThinking(alias, anthropicCompatible), - defaultThinkingEffort: deriveDefaultThinkingEffort(alias, anthropicCompatible), + thinkingSupported: deriveThinkingSupported(alias, providerType), + alwaysThinking: deriveAlwaysThinking(alias, providerType), + defaultThinkingEffort: deriveDefaultThinkingEffort(alias, providerType), }); } return out; } /** - * Provider-level Anthropic context for an alias, mirroring how - * `ProviderManager.resolveProviderConfig` resolves the wire protocol: the - * alias's provider (falling back to the configured default provider) decides - * when the alias itself does not declare `protocol`. Without this the catalog - * would mark a custom-named model on an `type = "anthropic"` provider as not - * thinking-capable while the runtime infers the latest Anthropic profile. + * The alias's provider type, resolved like + * `ProviderManager.resolveProviderConfig` does: the alias's provider (falling + * back to the configured default provider). The Anthropic fallback profile in + * `effectiveModelAlias` only applies to non-Kimi providers, so a custom-named + * model on a `type = "anthropic"` provider still gets an inferred effort list + * while managed Kimi models keep only their catalog-declared efforts. */ -function usesAnthropicProvider( +function providerTypeOf( alias: ModelAlias, config: { - providers?: Record; + providers?: Record; defaultProvider?: string | undefined; }, -): boolean { +): ProviderType | undefined { const providerName = alias.provider ?? config.defaultProvider; - if (providerName === undefined) return false; - return config.providers?.[providerName]?.type === 'anthropic'; + if (providerName === undefined) return undefined; + return config.providers?.[providerName]?.type; } diff --git a/packages/acp-adapter/test/config-options.test.ts b/packages/acp-adapter/test/config-options.test.ts index c65d67e745..08475497f3 100644 --- a/packages/acp-adapter/test/config-options.test.ts +++ b/packages/acp-adapter/test/config-options.test.ts @@ -17,27 +17,37 @@ function makeHarnessWithModels( displayName?: string; capabilities?: readonly string[]; protocol?: 'anthropic'; + providerType?: 'anthropic' | 'kimi' | 'openai'; }>, ): { harness: KimiHarness; getConfig: ReturnType } { // Mirror the `listAvailableModels` derivation: `id` is the config map // key, `model` defaults to id, `displayName` to model. The test fixtures // below pick names that exercise the three thinkingSupported triggers - // (name regex, capabilities array, toggleable allow-list). + // (name regex, capabilities array, toggleable allow-list). Entries with a + // `providerType` also get a backing provider so provider-aware derivation + // (e.g. the Anthropic fallback profile) can resolve the provider's type. const models: Record = {}; + const providers: Record = {}; for (const entry of entries) { + const providerName = `provider-${entry.id}`; models[entry.id] = { + ...(entry.providerType !== undefined ? { provider: providerName } : {}), model: entry.model ?? entry.id, ...(entry.displayName !== undefined ? { displayName: entry.displayName } : {}), ...(entry.capabilities !== undefined ? { capabilities: entry.capabilities } : {}), protocol: entry.protocol, }; + if (entry.providerType !== undefined) { + providers[providerName] = { type: entry.providerType }; + } } - const getConfig = vi.fn(async () => ({ models })); + const getConfig = vi.fn(async () => ({ models, providers })); return { harness: { getConfig } as unknown as KimiHarness, getConfig }; } @@ -175,6 +185,7 @@ describe('buildSessionConfigOptions', () => { id: 'custom', model: 'custom-anthropic-model', protocol: 'anthropic', + providerType: 'anthropic', }, ]); @@ -183,6 +194,21 @@ describe('buildSessionConfigOptions', () => { expect(result.map((option) => option.id)).toEqual(['model', 'thinking', 'mode']); }); + it('hides the thinking control for an unknown model on a Kimi provider using the Anthropic protocol', async () => { + const { harness } = makeHarnessWithModels([ + { + id: 'custom', + model: 'custom-anthropic-model', + protocol: 'anthropic', + providerType: 'kimi', + }, + ]); + + const result = await buildSessionConfigOptions(harness, 'custom', false, 'default'); + + expect(result.map((option) => option.id)).toEqual(['model', 'mode']); + }); + it('omits the thinking toggle when current model is non-thinking-supported', async () => { const { harness } = makeHarnessWithModels([ { id: 'kimi-coder', model: 'kimi-for-coding', displayName: 'Kimi Coder' }, diff --git a/packages/acp-adapter/test/model-catalog.test.ts b/packages/acp-adapter/test/model-catalog.test.ts index a99c40c0aa..5d5b3630e0 100644 --- a/packages/acp-adapter/test/model-catalog.test.ts +++ b/packages/acp-adapter/test/model-catalog.test.ts @@ -58,6 +58,9 @@ describe('listModelsFromHarness', () => { it('advertises thinking with a high default for an unknown model using the Anthropic protocol', async () => { const harness = { getConfig: async () => ({ + providers: { + custom: { type: 'anthropic' }, + }, models: { custom: { provider: 'custom', @@ -80,6 +83,34 @@ describe('listModelsFromHarness', () => { ]); }); + it('does not advertise thinking for an unknown model on a Kimi provider using the Anthropic protocol', async () => { + const harness = { + getConfig: async () => ({ + providers: { + 'managed:kimi-code': { type: 'kimi' }, + }, + models: { + custom: { + provider: 'managed:kimi-code', + model: 'custom-anthropic-model', + maxContextSize: 200000, + protocol: 'anthropic', + }, + }, + }), + } as unknown as KimiHarness; + + await expect(listModelsFromHarness(harness)).resolves.toEqual([ + { + id: 'custom', + name: 'custom-anthropic-model', + thinkingSupported: false, + alwaysThinking: false, + defaultThinkingEffort: 'on', + }, + ]); + }); + it('derives thinking support from the provider type when the alias omits protocol', async () => { // Same shape the runtime sees for `[providers.compat] type = "anthropic"` // + a custom-named model with no alias-level protocol: the provider diff --git a/packages/agent-core-v2/src/app/model/modelAuth.ts b/packages/agent-core-v2/src/app/model/modelAuth.ts index 6f768a9f80..35d224048f 100644 --- a/packages/agent-core-v2/src/app/model/modelAuth.ts +++ b/packages/agent-core-v2/src/app/model/modelAuth.ts @@ -13,7 +13,7 @@ import { matchKnownAnthropicModelProfile, } from '#/app/llmProtocol/providers/anthropic-profile'; import { type PlatformConfig, UNKNOWN_PLATFORM_KEY } from '#/app/platform/platform'; -import type { OAuthRef, ProviderConfig } from '#/app/provider/provider'; +import type { OAuthRef, ProviderConfig, ProviderType } from '#/app/provider/provider'; import type { Protocol } from '#/app/protocol/protocol'; import type { ModelConfig } from './model'; @@ -77,7 +77,7 @@ export function resolveModelAuthMaterial(args: { export function effectiveModelConfig( model: ModelConfig, - anthropicCompatible = false, + providerType?: ProviderType, ): ModelConfig { const { overrides, ...base } = model; const effective: ModelConfig = overrides === undefined ? model : { ...base, ...overrides }; @@ -89,18 +89,21 @@ export function effectiveModelConfig( ) { delete effective.defaultEffort; } - return withAnthropicProfile( - effective, - anthropicCompatible || effective.protocol === 'anthropic', - ); + return withAnthropicProfile(effective, providerType); } -function withAnthropicProfile(model: ModelConfig, anthropicCompatible: boolean): ModelConfig { +function withAnthropicProfile(model: ModelConfig, providerType?: ProviderType): ModelConfig { const wireName = model.name ?? model.model; + const protocol = model.protocol ?? providerType; + // The inferred fallback profile exists for third-party Anthropic-compatible + // endpoints whose model name encodes no known Claude version. Kimi providers + // — including managed models routed through protocol = "anthropic" — declare + // thinking efforts via the catalog, so they never receive the fallback. + // Callers without provider context fall back to name matching only. const profile = wireName === undefined ? undefined - : anthropicCompatible + : providerType !== undefined && providerType !== 'kimi' && protocol === 'anthropic' ? inferAnthropicModelProfile(wireName) : matchKnownAnthropicModelProfile(wireName); if (profile === undefined) return model; diff --git a/packages/agent-core-v2/src/app/model/modelResolverService.ts b/packages/agent-core-v2/src/app/model/modelResolverService.ts index f7019ae062..b19698c8bc 100644 --- a/packages/agent-core-v2/src/app/model/modelResolverService.ts +++ b/packages/agent-core-v2/src/app/model/modelResolverService.ts @@ -88,7 +88,13 @@ export class ModelResolverService extends Disposable implements IModelResolver { const { providerConfig, providerName, resolvedBaseUrl: rawBaseUrl } = this.resolveProviderContext(id, routingModel); const protocol = this.resolveProtocol(id, routingModel, providerConfig); - const model = effectiveModelConfig(configuredModel, protocol === 'anthropic'); + // For providerless models (inline base_url, no named provider) the model's + // own protocol declaration plays the provider-identity role when deciding + // whether the Anthropic fallback profile applies. + const model = effectiveModelConfig( + configuredModel, + providerConfig?.type ?? configuredModel.protocol, + ); const auth = resolveModelAuthMaterial({ modelId: id, model, diff --git a/packages/agent-core-v2/src/app/modelCatalog/modelCatalog.ts b/packages/agent-core-v2/src/app/modelCatalog/modelCatalog.ts index d8e58310a9..ffc6583a99 100644 --- a/packages/agent-core-v2/src/app/modelCatalog/modelCatalog.ts +++ b/packages/agent-core-v2/src/app/modelCatalog/modelCatalog.ts @@ -17,7 +17,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio import type { ModelAlias } from '#/app/model/model'; import { effectiveModelConfig } from '#/app/model/modelAuth'; -import type { ProviderConfig } from '#/app/provider/provider'; +import type { ProviderConfig, ProviderType } from '#/app/provider/provider'; export const modelCatalogItemSchema = z.object({ provider: z.string().min(1), @@ -107,9 +107,9 @@ export interface ProviderCredentialState { export function toProtocolModel( modelId: string, alias: ModelAlias, - anthropicCompatible = false, + providerType?: ProviderType, ): ModelCatalogItem { - const effective = effectiveModelConfig(alias, anthropicCompatible); + const effective = effectiveModelConfig(alias, providerType); return { provider: effective.provider ?? '', model: modelId, diff --git a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts index 101baa0b10..ef3e5927c8 100644 --- a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts +++ b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts @@ -37,6 +37,7 @@ import { IProviderService, type OAuthRef, type ProviderConfig, + type ProviderType, PROVIDERS_SECTION, } from '#/app/provider/provider'; @@ -68,7 +69,7 @@ export class ModelCatalogService implements IModelCatalogService { async listModels(): Promise { const models = this.modelService.list(); return Object.entries(models).map(([modelId, alias]) => - toProtocolModel(modelId, alias, this.usesAnthropicProtocol(alias)), + toProtocolModel(modelId, alias, this.providerTypeOf(alias)), ); } @@ -102,14 +103,14 @@ export class ModelCatalogService implements IModelCatalogService { const updatedAlias = this.modelService.get(modelId) ?? alias; return { default_model: modelId, - model: toProtocolModel(modelId, updatedAlias, this.usesAnthropicProtocol(updatedAlias)), + model: toProtocolModel(modelId, updatedAlias, this.providerTypeOf(updatedAlias)), }; } - private usesAnthropicProtocol(alias: ModelAlias): boolean { + private providerTypeOf(alias: ModelAlias): ProviderType | undefined { const providerId = alias.providerId ?? alias.provider ?? this.config.get('defaultProvider'); - return (alias.protocol ?? this.providerService.get(providerId ?? '')?.type) === 'anthropic'; + return this.providerService.get(providerId ?? '')?.type; } refreshProviderModels( diff --git a/packages/agent-core-v2/test/app/model/model.test.ts b/packages/agent-core-v2/test/app/model/model.test.ts index 49ab54742d..b6c2f50774 100644 --- a/packages/agent-core-v2/test/app/model/model.test.ts +++ b/packages/agent-core-v2/test/app/model/model.test.ts @@ -21,6 +21,7 @@ import { modelsFromToml, modelsToToml } from '#/app/model/configSection'; import { ModelService } from '#/app/model/modelService'; import { ENV_MODEL_PROVIDER_KEY } from '#/app/provider/provider'; import { effectiveModelConfig } from '#/app/model/modelAuth'; +import type { ModelConfig } from '#/app/model/model'; describe('effectiveModelConfig', () => { it('derives the official effort metadata from a Claude model name', () => { @@ -37,14 +38,17 @@ describe('effectiveModelConfig', () => { }); }); - it('infers Anthropic effort metadata for an unknown model with an explicit Anthropic protocol', () => { + it('infers Anthropic effort metadata for an unknown model on a non-Kimi Anthropic provider', () => { expect( - effectiveModelConfig({ - provider: 'custom', - model: 'custom-anthropic-model', - maxContextSize: 200000, - protocol: 'anthropic', - }), + effectiveModelConfig( + { + provider: 'custom', + model: 'custom-anthropic-model', + maxContextSize: 200000, + protocol: 'anthropic', + }, + 'anthropic', + ), ).toMatchObject({ capabilities: ['thinking'], supportEfforts: ['low', 'medium', 'high', 'xhigh', 'max'], @@ -52,15 +56,42 @@ describe('effectiveModelConfig', () => { }); }); + it('does not infer Anthropic effort metadata for a Kimi provider routed through the Anthropic protocol', () => { + const model: ModelConfig = { + provider: 'managed:kimi-code', + model: 'kimi-for-coding', + maxContextSize: 262144, + capabilities: ['thinking', 'always_thinking'], + protocol: 'anthropic', + adaptiveThinking: true, + }; + + expect(effectiveModelConfig(model, 'kimi')).toEqual(model); + }); + + it('does not infer the fallback profile without provider context', () => { + const model: ModelConfig = { + provider: 'custom', + model: 'custom-anthropic-model', + maxContextSize: 200000, + protocol: 'anthropic', + }; + + expect(effectiveModelConfig(model)).toEqual(model); + }); + it('limits an adaptive_thinking=false model to budget efforts', () => { expect( - effectiveModelConfig({ - provider: 'custom', - model: 'custom-anthropic-model', - maxContextSize: 200000, - protocol: 'anthropic', - adaptiveThinking: false, - }), + effectiveModelConfig( + { + provider: 'custom', + model: 'custom-anthropic-model', + maxContextSize: 200000, + protocol: 'anthropic', + adaptiveThinking: false, + }, + 'anthropic', + ), ).toMatchObject({ capabilities: ['thinking'], supportEfforts: ['low', 'medium', 'high'], diff --git a/packages/agent-core-v2/test/app/model/modelResolver.test.ts b/packages/agent-core-v2/test/app/model/modelResolver.test.ts index 0c169a245c..e068548e2d 100644 --- a/packages/agent-core-v2/test/app/model/modelResolver.test.ts +++ b/packages/agent-core-v2/test/app/model/modelResolver.test.ts @@ -654,7 +654,7 @@ describe('ModelResolverService', () => { }); }); - it('infers latest Opus metadata for an unknown Kimi-managed Anthropic model', () => { + it('does not infer fallback effort metadata for an unknown Kimi-managed Anthropic model', () => { providers['p'] = { type: 'kimi', baseUrl: 'https://example.test', apiKey: 'sk' }; models['m'] = { provider: 'p', @@ -665,8 +665,8 @@ describe('ModelResolverService', () => { const model = ix.get(IModelResolver).resolve('m'); - expect(model.supportEfforts).toEqual(['low', 'medium', 'high', 'xhigh', 'max']); - expect(model.defaultEffort).toBe('high'); + expect(model.supportEfforts).toBeUndefined(); + expect(model.defaultEffort).toBeUndefined(); }); it('passes Vertex service-account options and derives location from the baseUrl', async () => { diff --git a/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts b/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts index 5f0848edaa..bf45a24a8f 100644 --- a/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts +++ b/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts @@ -189,10 +189,10 @@ describe('ModelCatalogService', () => { }); }); - it('projects latest Opus efforts for unknown Kimi-managed Anthropic models', async () => { + it('projects latest Opus efforts for unknown Anthropic-compatible models', async () => { + backing.providers['custom'] = { type: 'anthropic' }; backing.models['compatible'] = { - provider: 'kimi', - protocol: 'anthropic', + provider: 'custom', model: 'compatible-model', maxContextSize: 128000, }; @@ -207,6 +207,26 @@ describe('ModelCatalogService', () => { }); }); + it('does not project fallback efforts for unknown Kimi-managed Anthropic models', async () => { + backing.models['compatible'] = { + provider: 'kimi', + protocol: 'anthropic', + model: 'compatible-model', + maxContextSize: 128000, + }; + + const compatible = (await catalog().listModels()).find( + (model) => model.model === 'compatible', + ); + expect(compatible).toMatchObject({ + provider: 'kimi', + model: 'compatible', + }); + expect(compatible?.capabilities).toBeUndefined(); + expect(compatible?.support_efforts).toBeUndefined(); + expect(compatible?.default_effort).toBeUndefined(); + }); + it('projects effort fields from overrides when present', async () => { backing.models['k2'] = { ...backing.models['k2'], diff --git a/packages/agent-core/src/config/model.ts b/packages/agent-core/src/config/model.ts index 3411965691..b8a19f5a31 100644 --- a/packages/agent-core/src/config/model.ts +++ b/packages/agent-core/src/config/model.ts @@ -4,11 +4,11 @@ import { matchKnownAnthropicModelProfile, } from '@moonshot-ai/kosong/providers/anthropic-profile'; -import type { ModelAlias } from './schema'; +import type { ModelAlias, ProviderType } from './schema'; export function effectiveModelAlias( alias: ModelAlias, - anthropicCompatible = false, + providerType?: ProviderType, ): ModelAlias { const { overrides, ...base } = alias; const effective: ModelAlias = overrides === undefined ? alias : { ...base, ...overrides }; @@ -22,16 +22,20 @@ export function effectiveModelAlias( delete effective.defaultEffort; } - return withAnthropicProfile( - effective, - anthropicCompatible || effective.protocol === 'anthropic', - ); + return withAnthropicProfile(effective, providerType); } -function withAnthropicProfile(model: ModelAlias, anthropicCompatible: boolean): ModelAlias { - const profile = anthropicCompatible - ? inferAnthropicModelProfile(model.model) - : matchKnownAnthropicModelProfile(model.model); +function withAnthropicProfile(model: ModelAlias, providerType?: ProviderType): ModelAlias { + const protocol = model.protocol ?? providerType; + // The inferred fallback profile exists for third-party Anthropic-compatible + // endpoints whose model name encodes no known Claude version. Kimi providers + // — including managed models routed through protocol = "anthropic" — declare + // thinking efforts via the catalog, so they never receive the fallback. + // Callers without provider context fall back to name matching only. + const profile = + providerType !== undefined && providerType !== 'kimi' && protocol === 'anthropic' + ? inferAnthropicModelProfile(model.model) + : matchKnownAnthropicModelProfile(model.model); if (profile === undefined) return model; const capability = profile.canDisableThinking ? 'thinking' : 'always_thinking'; diff --git a/packages/agent-core/src/services/modelCatalog/modelCatalog.ts b/packages/agent-core/src/services/modelCatalog/modelCatalog.ts index 3944cdd80d..c47532be1c 100644 --- a/packages/agent-core/src/services/modelCatalog/modelCatalog.ts +++ b/packages/agent-core/src/services/modelCatalog/modelCatalog.ts @@ -1,5 +1,5 @@ import { createDecorator } from '../../di'; -import { effectiveModelAlias, type KimiConfig, type ModelAlias, type ProviderConfig } from '../../config'; +import { effectiveModelAlias, type KimiConfig, type ModelAlias, type ProviderConfig, type ProviderType } from '../../config'; import type { ModelCatalogItem, ProviderCatalogItem, @@ -57,9 +57,9 @@ export class ModelNotFoundError extends Error { export function toProtocolModel( modelId: string, alias: ModelAlias, - anthropicCompatible = false, + providerType?: ProviderType, ): ModelCatalogItem { - const effective = effectiveModelAlias(alias, anthropicCompatible); + const effective = effectiveModelAlias(alias, providerType); return { provider: effective.provider, model: modelId, diff --git a/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts b/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts index 1b5f8460b8..db70fdebf5 100644 --- a/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts +++ b/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts @@ -1,5 +1,5 @@ import { Disposable, InstantiationType, registerSingleton } from '../../di'; -import type { KimiConfig, ModelAlias, ProviderConfig } from '../../config'; +import type { KimiConfig, ModelAlias, ProviderConfig, ProviderType } from '../../config'; import type { ModelCatalogItem, ProviderCatalogItem, @@ -61,7 +61,7 @@ export class ModelCatalogService async listModels(): Promise { const config = await this._readConfig(); return Object.entries(config.models ?? {}).map(([modelId, alias]) => - toProtocolModel(modelId, alias, this._usesAnthropicProtocol(config, alias)), + toProtocolModel(modelId, alias, this._providerTypeOf(config, alias)), ); } @@ -97,14 +97,14 @@ export class ModelCatalogService model: toProtocolModel( modelId, updatedAlias, - this._usesAnthropicProtocol(updated, updatedAlias), + this._providerTypeOf(updated, updatedAlias), ), }; } - private _usesAnthropicProtocol(config: KimiConfig, alias: ModelAlias): boolean { + private _providerTypeOf(config: KimiConfig, alias: ModelAlias): ProviderType | undefined { const providerId = alias.provider ?? config.defaultProvider; - return (alias.protocol ?? config.providers[providerId ?? '']?.type) === 'anthropic'; + return config.providers[providerId ?? '']?.type; } async refreshOAuthProviderModels(): Promise { diff --git a/packages/agent-core/src/session/provider-manager.ts b/packages/agent-core/src/session/provider-manager.ts index 14d2b5ba18..4a4bdcdc72 100644 --- a/packages/agent-core/src/session/provider-manager.ts +++ b/packages/agent-core/src/session/provider-manager.ts @@ -113,8 +113,7 @@ export class ProviderManager implements ModelProvider { ); } - const protocol = alias.protocol ?? providerConfig.type; - const effectiveAlias = effectiveModelAlias(alias, protocol === 'anthropic'); + const effectiveAlias = effectiveModelAlias(alias, providerConfig.type); if (!Number.isInteger(effectiveAlias.maxContextSize) || effectiveAlias.maxContextSize <= 0) { throw new KimiError( diff --git a/packages/agent-core/test/config/model-overrides.test.ts b/packages/agent-core/test/config/model-overrides.test.ts index 40ab1186e9..9c06e37bba 100644 --- a/packages/agent-core/test/config/model-overrides.test.ts +++ b/packages/agent-core/test/config/model-overrides.test.ts @@ -60,7 +60,7 @@ describe('effectiveModelAlias', () => { }); }); - it('infers Anthropic effort metadata for an unknown model with an explicit Anthropic protocol', () => { + it('infers Anthropic effort metadata for an unknown model on a non-Kimi Anthropic provider', () => { const model: ModelAlias = { provider: 'custom', model: 'custom-anthropic-model', @@ -68,13 +68,37 @@ describe('effectiveModelAlias', () => { protocol: 'anthropic', }; - expect(effectiveModelAlias(model)).toMatchObject({ + expect(effectiveModelAlias(model, 'anthropic')).toMatchObject({ capabilities: ['thinking'], supportEfforts: ['low', 'medium', 'high', 'xhigh', 'max'], defaultEffort: 'high', }); }); + it('does not infer Anthropic effort metadata for a Kimi provider routed through the Anthropic protocol', () => { + const model: ModelAlias = { + provider: 'managed:kimi-code', + model: 'kimi-for-coding', + maxContextSize: 262144, + capabilities: ['thinking', 'always_thinking'], + protocol: 'anthropic', + adaptiveThinking: true, + }; + + expect(effectiveModelAlias(model, 'kimi')).toEqual(model); + }); + + it('does not infer the fallback profile without provider context', () => { + const model: ModelAlias = { + provider: 'custom', + model: 'custom-anthropic-model', + maxContextSize: 200000, + protocol: 'anthropic', + }; + + expect(effectiveModelAlias(model)).toEqual(model); + }); + it('limits an adaptive_thinking=false model to budget efforts', () => { const model: ModelAlias = { provider: 'custom', @@ -84,7 +108,7 @@ describe('effectiveModelAlias', () => { adaptiveThinking: false, }; - expect(effectiveModelAlias(model)).toMatchObject({ + expect(effectiveModelAlias(model, 'anthropic')).toMatchObject({ capabilities: ['thinking'], supportEfforts: ['low', 'medium', 'high'], defaultEffort: 'high', @@ -101,7 +125,7 @@ describe('effectiveModelAlias', () => { supportEfforts: ['low', 'high'], }; - expect(effectiveModelAlias(model)).toMatchObject({ + expect(effectiveModelAlias(model, 'anthropic')).toMatchObject({ capabilities: ['thinking'], supportEfforts: ['low', 'high'], defaultEffort: 'high', diff --git a/packages/agent-core/test/services/model-catalog-service.test.ts b/packages/agent-core/test/services/model-catalog-service.test.ts index e50736ad16..96a0d013ab 100644 --- a/packages/agent-core/test/services/model-catalog-service.test.ts +++ b/packages/agent-core/test/services/model-catalog-service.test.ts @@ -211,9 +211,9 @@ describe('ModelCatalogService', () => { it('projects latest Opus efforts for unknown Anthropic-compatible models', async () => { const configRef = { current: catalogConfig() }; + configRef.current.providers['custom'] = { type: 'anthropic' }; configRef.current.models!['compatible'] = { - provider: 'kimi', - protocol: 'anthropic', + provider: 'custom', model: 'compatible-model', maxContextSize: 128000, }; @@ -228,6 +228,27 @@ describe('ModelCatalogService', () => { }); }); + it('does not project fallback efforts for a Kimi provider routed through the Anthropic protocol', async () => { + const configRef = { current: catalogConfig() }; + configRef.current.models!['compatible'] = { + provider: 'kimi', + protocol: 'anthropic', + model: 'compatible-model', + maxContextSize: 128000, + }; + const { core } = makeCore(configRef); + const svc = new ModelCatalogService(makeEnv(), core, makeEventService().svc); + + const compatible = (await svc.listModels()).find((model) => model.model === 'compatible'); + expect(compatible).toMatchObject({ + provider: 'kimi', + model: 'compatible', + }); + expect(compatible?.capabilities).toBeUndefined(); + expect(compatible?.support_efforts).toBeUndefined(); + expect(compatible?.default_effort).toBeUndefined(); + }); + it('gets one provider or throws ProviderNotFoundError', async () => { const configRef = { current: catalogConfig() }; const { core } = makeCore(configRef); From 56747c8e6127da38dc6f66268cc805a6d04776a7 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Thu, 16 Jul 2026 16:13:49 +0800 Subject: [PATCH 2/4] fix: align v2 catalog with resolver for providerless Anthropic models Flat models (inline base_url, no named provider) and providers without a declared type now fall back to the model's own protocol when deciding the Anthropic fallback effort profile, so the model catalog stays consistent with runtime resolution. --- .../app/modelCatalog/modelCatalogService.ts | 5 ++++- .../test/app/model/modelResolver.test.ts | 14 ++++++++++++++ .../test/app/modelCatalog/modelCatalog.test.ts | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts index ef3e5927c8..28800ad48e 100644 --- a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts +++ b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts @@ -110,7 +110,10 @@ export class ModelCatalogService implements IModelCatalogService { private providerTypeOf(alias: ModelAlias): ProviderType | undefined { const providerId = alias.providerId ?? alias.provider ?? this.config.get('defaultProvider'); - return this.providerService.get(providerId ?? '')?.type; + // Flat models (inline base_url, no named provider) have no provider entry + // to look up; their own protocol declaration plays the provider-identity + // role, mirroring ModelResolverService. + return this.providerService.get(providerId ?? '')?.type ?? alias.protocol; } refreshProviderModels( diff --git a/packages/agent-core-v2/test/app/model/modelResolver.test.ts b/packages/agent-core-v2/test/app/model/modelResolver.test.ts index e068548e2d..790f715c34 100644 --- a/packages/agent-core-v2/test/app/model/modelResolver.test.ts +++ b/packages/agent-core-v2/test/app/model/modelResolver.test.ts @@ -669,6 +669,20 @@ describe('ModelResolverService', () => { expect(model.defaultEffort).toBeUndefined(); }); + it('infers latest Opus metadata for a flat providerless Anthropic model', () => { + models['m'] = { + model: 'compatible-model', + baseUrl: 'https://anthropic.example.test', + protocol: 'anthropic', + maxContextSize: 1000, + }; + + const model = ix.get(IModelResolver).resolve('m'); + + expect(model.supportEfforts).toEqual(['low', 'medium', 'high', 'xhigh', 'max']); + expect(model.defaultEffort).toBe('high'); + }); + it('passes Vertex service-account options and derives location from the baseUrl', async () => { providers['p'] = { type: 'vertexai', diff --git a/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts b/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts index bf45a24a8f..3b95dc2a27 100644 --- a/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts +++ b/packages/agent-core-v2/test/app/modelCatalog/modelCatalog.test.ts @@ -207,6 +207,24 @@ describe('ModelCatalogService', () => { }); }); + it('projects latest Opus efforts for a flat providerless Anthropic model', async () => { + backing.models['compatible'] = { + model: 'compatible-model', + baseUrl: 'https://anthropic.example.test', + protocol: 'anthropic', + maxContextSize: 128000, + }; + + const compatible = (await catalog().listModels()).find( + (model) => model.model === 'compatible', + ); + expect(compatible).toMatchObject({ + capabilities: ['thinking'], + support_efforts: ['low', 'medium', 'high', 'xhigh', 'max'], + default_effort: 'high', + }); + }); + it('does not project fallback efforts for unknown Kimi-managed Anthropic models', async () => { backing.models['compatible'] = { provider: 'kimi', From 0ac9577b30e97c161cf08a299a3dd240046fcbfd Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Thu, 16 Jul 2026 16:38:17 +0800 Subject: [PATCH 3/4] fix: keep flat Anthropic model effort metadata in TUI and ACP catalogs Flat models without a named provider (inline base_url, protocol: "anthropic") have no provider entry to look up; fall back to the model's own protocol as the provider identity so the effort picker and ACP catalog stay consistent with runtime resolution. --- apps/kimi-code/src/tui/commands/config.ts | 5 +++- .../test/tui/kimi-tui-message-flow.test.ts | 26 +++++++++++++++++++ packages/acp-adapter/src/model-catalog.ts | 8 ++++-- .../acp-adapter/test/model-catalog.test.ts | 24 +++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 807dfa671c..375e6b98fb 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -47,7 +47,10 @@ function currentTuiConfig(host: SlashCommandHost): TuiConfig { function effectiveModelForHost(host: SlashCommandHost, model: ModelAlias): ModelAlias { const providerType = host.state.appState.availableProviders[model.provider]?.type; - return effectiveModelAlias(model, providerType); + // Flat models (no named provider, e.g. inline base_url served by a v2 + // backend) have no provider entry to look up; their own protocol declaration + // plays the provider-identity role, mirroring the resolver. + return effectiveModelAlias(model, providerType ?? model.protocol); } export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise { diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 6e0a6b8b25..dc726e99f8 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -5254,6 +5254,32 @@ describe('/effort support_efforts override', () => { expect(picker.render(80).join('\n')).not.toContain('Max'); }); + it('offers the latest Opus efforts for a flat providerless Anthropic model', async () => { + const { driver } = await makeDriver(makeSession(), { + getConfig: vi.fn(async () => ({ + providers: {}, + models: { + // v2 flat model shape: no named provider, inline endpoint + protocol. + k2: { + model: 'compatible-model', + baseUrl: 'https://anthropic.example.test', + protocol: 'anthropic', + maxContextSize: 100, + }, + }, + defaultModel: 'k2', + })), + }); + + driver.handleUserInput('/effort'); + + await vi.waitFor(() => { + expect(driver.state.editorContainer.children[0]).toBeInstanceOf(EffortSelectorComponent); + }); + const picker = driver.state.editorContainer.children[0] as EffortSelectorComponent; + expect(picker.render(80).join('\n')).toContain('Max'); + }); + it('keeps rejecting efforts hidden by a Kimi support_efforts override', async () => { const session = makeSession(); const { driver } = await makeDriver(session, { diff --git a/packages/acp-adapter/src/model-catalog.ts b/packages/acp-adapter/src/model-catalog.ts index dbf80bc61d..a3c87a6384 100644 --- a/packages/acp-adapter/src/model-catalog.ts +++ b/packages/acp-adapter/src/model-catalog.ts @@ -155,6 +155,10 @@ function providerTypeOf( }, ): ProviderType | undefined { const providerName = alias.provider ?? config.defaultProvider; - if (providerName === undefined) return undefined; - return config.providers?.[providerName]?.type; + const providerType = + providerName === undefined ? undefined : config.providers?.[providerName]?.type; + // Flat models (inline base_url, no named provider) have no provider entry to + // look up; their own protocol declaration plays the provider-identity role, + // mirroring the v2 ModelResolverService. + return providerType ?? alias.protocol; } diff --git a/packages/acp-adapter/test/model-catalog.test.ts b/packages/acp-adapter/test/model-catalog.test.ts index 5d5b3630e0..bfa852ac82 100644 --- a/packages/acp-adapter/test/model-catalog.test.ts +++ b/packages/acp-adapter/test/model-catalog.test.ts @@ -83,6 +83,30 @@ describe('listModelsFromHarness', () => { ]); }); + it('advertises thinking for a flat providerless model using the Anthropic protocol', async () => { + const harness = { + getConfig: async () => ({ + models: { + custom: { + model: 'custom-anthropic-model', + maxContextSize: 200000, + protocol: 'anthropic', + }, + }, + }), + } as unknown as KimiHarness; + + await expect(listModelsFromHarness(harness)).resolves.toEqual([ + { + id: 'custom', + name: 'custom-anthropic-model', + thinkingSupported: true, + alwaysThinking: false, + defaultThinkingEffort: 'high', + }, + ]); + }); + it('does not advertise thinking for an unknown model on a Kimi provider using the Anthropic protocol', async () => { const harness = { getConfig: async () => ({ From 9811bcadd99130e94cbb4edb060e8c301650306a Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Thu, 16 Jul 2026 17:19:11 +0800 Subject: [PATCH 4/4] style: move v2 anthropic fallback rationale into the modelAuth header The v2 comment convention keeps comments in the top-of-file block only; drop the inline explanations added beside functions and statements. --- packages/agent-core-v2/src/app/model/modelAuth.ts | 9 ++++----- .../agent-core-v2/src/app/model/modelResolverService.ts | 3 --- .../src/app/modelCatalog/modelCatalogService.ts | 3 --- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/agent-core-v2/src/app/model/modelAuth.ts b/packages/agent-core-v2/src/app/model/modelAuth.ts index 35d224048f..852b864a46 100644 --- a/packages/agent-core-v2/src/app/model/modelAuth.ts +++ b/packages/agent-core-v2/src/app/model/modelAuth.ts @@ -4,6 +4,10 @@ * Resolves Model / Provider / Platform credential precedence for runtime * model resolution and auth-readiness probes. Pure computation; callers * supply the Platform lookup so this file stays outside the service graph. + * The inferred Anthropic effort profile is reserved for non-Kimi + * Anthropic-compatible providers; Kimi providers — including managed models + * routed through protocol = "anthropic" — keep only catalog-declared effort + * metadata. */ import { ErrorCodes, Error2 } from '#/errors'; @@ -95,11 +99,6 @@ export function effectiveModelConfig( function withAnthropicProfile(model: ModelConfig, providerType?: ProviderType): ModelConfig { const wireName = model.name ?? model.model; const protocol = model.protocol ?? providerType; - // The inferred fallback profile exists for third-party Anthropic-compatible - // endpoints whose model name encodes no known Claude version. Kimi providers - // — including managed models routed through protocol = "anthropic" — declare - // thinking efforts via the catalog, so they never receive the fallback. - // Callers without provider context fall back to name matching only. const profile = wireName === undefined ? undefined diff --git a/packages/agent-core-v2/src/app/model/modelResolverService.ts b/packages/agent-core-v2/src/app/model/modelResolverService.ts index b19698c8bc..a7d7b1e9af 100644 --- a/packages/agent-core-v2/src/app/model/modelResolverService.ts +++ b/packages/agent-core-v2/src/app/model/modelResolverService.ts @@ -88,9 +88,6 @@ export class ModelResolverService extends Disposable implements IModelResolver { const { providerConfig, providerName, resolvedBaseUrl: rawBaseUrl } = this.resolveProviderContext(id, routingModel); const protocol = this.resolveProtocol(id, routingModel, providerConfig); - // For providerless models (inline base_url, no named provider) the model's - // own protocol declaration plays the provider-identity role when deciding - // whether the Anthropic fallback profile applies. const model = effectiveModelConfig( configuredModel, providerConfig?.type ?? configuredModel.protocol, diff --git a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts index 28800ad48e..37a8062edf 100644 --- a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts +++ b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts @@ -110,9 +110,6 @@ export class ModelCatalogService implements IModelCatalogService { private providerTypeOf(alias: ModelAlias): ProviderType | undefined { const providerId = alias.providerId ?? alias.provider ?? this.config.get('defaultProvider'); - // Flat models (inline base_url, no named provider) have no provider entry - // to look up; their own protocol declaration plays the provider-identity - // role, mirroring ModelResolverService. return this.providerService.get(providerId ?? '')?.type ?? alias.protocol; }