Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-provider-default-base-url.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/agent-core-v2/src/app/model/modelImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>;
readonly capabilities: ModelCapability;
readonly maxContextSize: number;
Expand All @@ -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<Record<string, string>>;
readonly capabilities: ModelCapability;
readonly maxContextSize: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/model/modelInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>;

readonly capabilities: ModelCapability;
Expand Down
19 changes: 9 additions & 10 deletions packages/agent-core-v2/src/app/model/modelResolverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -201,15 +203,18 @@ 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.<providerId>]`; 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,
model: ModelConfig,
): {
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`,
Expand All @@ -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 };
}

Expand Down Expand Up @@ -360,7 +359,7 @@ function buildProtocolProviderOptions(
model: ModelConfig,
protocol: Protocol,
provider: ProviderConfig | undefined,
baseUrl: string,
baseUrl: string | undefined,
): ProtocolProviderOptions | undefined {
const options: MutableProtocolProviderOptions = {};

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/protocol/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>;
Expand Down
36 changes: 35 additions & 1 deletion packages/agent-core-v2/test/app/model/modelResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.',
);
});
});
});

Expand Down
Loading