From ba87e6c7a1f3d1e1da34da3e7c3dc551a7ab1fbd Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Mon, 13 Jul 2026 23:10:28 +0800 Subject: [PATCH] fix(agent-core-v2): fall back to protocol default base URL when unconfigured - drop the "missing a base URL" rejection in ModelResolverService so a structured provider without base_url resolves to undefined and the wire provider applies its protocol default endpoint, matching v1 - relax baseUrl to optional across Model, ModelImplInit, and ProtocolAdapterConfig; guard the anthropic /v1 strip against undefined --- .changeset/fix-provider-default-base-url.md | 6 ++++ .../agent-core-v2/src/app/model/modelImpl.ts | 4 +-- .../src/app/model/modelInstance.ts | 2 +- .../src/app/model/modelResolverService.ts | 19 +++++----- .../src/app/protocol/protocol.ts | 2 +- .../test/app/model/modelResolver.test.ts | 36 ++++++++++++++++++- 6 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 .changeset/fix-provider-default-base-url.md diff --git a/.changeset/fix-provider-default-base-url.md b/.changeset/fix-provider-default-base-url.md new file mode 100644 index 0000000000..38e306b23b --- /dev/null +++ b/.changeset/fix-provider-default-base-url.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core-v2": patch +"@moonshot-ai/kimi-code": patch +--- + +Fix providers without a configured base_url being rejected: anthropic/openai and other protocol providers now fall back to their official default endpoints again, as before. diff --git a/packages/agent-core-v2/src/app/model/modelImpl.ts b/packages/agent-core-v2/src/app/model/modelImpl.ts index 3c68f457f2..a667abb498 100644 --- a/packages/agent-core-v2/src/app/model/modelImpl.ts +++ b/packages/agent-core-v2/src/app/model/modelImpl.ts @@ -39,7 +39,7 @@ export interface ModelImplInit { readonly name: string; readonly aliases: readonly string[]; readonly protocol: Protocol; - readonly baseUrl: string; + readonly baseUrl?: string; readonly headers: Readonly>; readonly capabilities: ModelCapability; readonly maxContextSize: number; @@ -60,7 +60,7 @@ export class ModelImpl implements Model { readonly name: string; readonly aliases: readonly string[]; readonly protocol: Protocol; - readonly baseUrl: string; + readonly baseUrl: string | undefined; readonly headers: Readonly>; readonly capabilities: ModelCapability; readonly maxContextSize: number; diff --git a/packages/agent-core-v2/src/app/model/modelInstance.ts b/packages/agent-core-v2/src/app/model/modelInstance.ts index 6e19d07cb1..6cbb052e1f 100644 --- a/packages/agent-core-v2/src/app/model/modelInstance.ts +++ b/packages/agent-core-v2/src/app/model/modelInstance.ts @@ -87,7 +87,7 @@ export interface Model { /** Free-form routing aliases; a name-based lookup matches these. */ readonly aliases: readonly string[]; readonly protocol: Protocol; - readonly baseUrl: string; + readonly baseUrl?: string; readonly headers: Readonly>; readonly capabilities: ModelCapability; diff --git a/packages/agent-core-v2/src/app/model/modelResolverService.ts b/packages/agent-core-v2/src/app/model/modelResolverService.ts index 00d0deea19..653e0d41f8 100644 --- a/packages/agent-core-v2/src/app/model/modelResolverService.ts +++ b/packages/agent-core-v2/src/app/model/modelResolverService.ts @@ -99,7 +99,9 @@ export class ModelResolverService extends Disposable implements IModelResolver { // overrides into the Anthropic transport. Native Anthropic providers keep // their configured `/v1` because the old provider manager did too. const resolvedBaseUrl = - model.protocol === 'anthropic' ? stripTrailingV1(rawBaseUrl) : rawBaseUrl; + model.protocol === 'anthropic' && rawBaseUrl !== undefined + ? stripTrailingV1(rawBaseUrl) + : rawBaseUrl; const wireName = model.name ?? model.model; if (wireName === undefined) { throw new Error2( @@ -201,7 +203,10 @@ export class ModelResolverService extends Disposable implements IModelResolver { /** * Return the ProviderConfig this Model resolves against, plus the URL to * hit at runtime. Structured path reads `[providers.]`; flat - * path synthesizes a Provider record from the Model's inline baseUrl. + * path synthesizes a Provider record from the Model's inline baseUrl. A + * structured Model with no baseUrl anywhere (config, provider, env) yields + * `undefined` — the wire provider then applies its protocol default + * endpoint, matching v1's `provider-manager`. */ private resolveProviderContext( id: string, @@ -209,7 +214,7 @@ export class ModelResolverService extends Disposable implements IModelResolver { ): { readonly providerConfig: ProviderConfig | undefined; readonly providerName: string; - readonly resolvedBaseUrl: string; + readonly resolvedBaseUrl: string | undefined; } { // Structured path — Model references a Provider (which may reference a // Platform). Legacy configs still use `provider` in place of `providerId`, @@ -232,12 +237,6 @@ export class ModelResolverService extends Disposable implements IModelResolver { model.protocol ?? providerConfig.type, providerConfig.env, ); - if (baseUrl === undefined || baseUrl.length === 0) { - throw new Error2( - ErrorCodes.CONFIG_INVALID, - `Model "${id}" (via provider "${providerId}") is missing a base URL.`, - ); - } return { providerConfig, providerName: providerId, resolvedBaseUrl: baseUrl }; } @@ -360,7 +359,7 @@ function buildProtocolProviderOptions( model: ModelConfig, protocol: Protocol, provider: ProviderConfig | undefined, - baseUrl: string, + baseUrl: string | undefined, ): ProtocolProviderOptions | undefined { const options: MutableProtocolProviderOptions = {}; diff --git a/packages/agent-core-v2/src/app/protocol/protocol.ts b/packages/agent-core-v2/src/app/protocol/protocol.ts index 3bfb234382..f1499d1894 100644 --- a/packages/agent-core-v2/src/app/protocol/protocol.ts +++ b/packages/agent-core-v2/src/app/protocol/protocol.ts @@ -51,7 +51,7 @@ export interface ProtocolProviderOptions { */ export interface ProtocolAdapterConfig { readonly protocol: Protocol; - readonly baseUrl: string; + readonly baseUrl?: string; readonly modelName: string; readonly apiKey?: string; readonly defaultHeaders?: Readonly>; 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 7fc9eaf64c..eab39b0451 100644 --- a/packages/agent-core-v2/test/app/model/modelResolver.test.ts +++ b/packages/agent-core-v2/test/app/model/modelResolver.test.ts @@ -816,7 +816,11 @@ describe('ModelResolverService', () => { ); }); - function resolveBaseUrl(protocol: string, providerType: string, baseUrl: string): string { + function resolveBaseUrl( + protocol: string, + providerType: string, + baseUrl: string, + ): string | undefined { providers['p'] = { type: providerType, baseUrl, apiKey: 'sk' } as ProviderConfig; models['m'] = { provider: 'p', model: 'wire-name', maxContextSize: 1000, protocol } as ModelConfig; return ix.get(IModelResolver).resolve('m').baseUrl; @@ -852,6 +856,36 @@ describe('ModelResolverService', () => { 'https://example.test/coding/v1', ); }); + + it.each(['anthropic', 'openai', 'openai_responses', 'kimi', 'google-genai'] as const)( + 'resolves a %s provider without base_url to an undefined baseUrl (protocol default applies)', + (type) => { + providers['p'] = { type, apiKey: 'sk' }; + models['m'] = { provider: 'p', model: 'wire-name', maxContextSize: 1000 }; + + expect(ix.get(IModelResolver).resolve('m').baseUrl).toBeUndefined(); + }, + ); + + it('resolves an anthropic-protocol override without base_url without stripping', () => { + providers['p'] = { type: 'kimi', apiKey: 'sk' }; + models['m'] = { + provider: 'p', + model: 'wire-name', + maxContextSize: 1000, + protocol: 'anthropic', + }; + + expect(ix.get(IModelResolver).resolve('m').baseUrl).toBeUndefined(); + }); + + it('still rejects a flat model with neither providerId nor baseUrl', () => { + models['m'] = { model: 'wire-name', maxContextSize: 1000 }; + + expect(() => ix.get(IModelResolver).resolve('m')).toThrow( + 'Model "m" must set either providerId or baseUrl in config.toml.', + ); + }); }); });