Skip to content
Open
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-oauth-login-default-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kimi-code": patch
"@moonshot-ai/kimi-code-sdk": patch
---

Fix OAuth login retaining an API key model as the default after switching back to Kimi Code.
21 changes: 20 additions & 1 deletion packages/node-sdk/test/auth-facade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,23 @@ oauth = { storage = "file", key = "${oauthKey}", oauth_host = "https://auth.dev.
});
});

it('provisions SDK config using an existing Kimi OAuth token', async () => {
it('switches an API default when logging in with an existing Kimi OAuth token', async () => {
await new FileTokenStorage(join(homeDir, 'credentials')).save('kimi-code', freshToken());
await writeFile(
join(homeDir, 'config.toml'),
`
default_model = "api-provider/api-model"

[providers.api-provider]
type = "kimi"
api_key = "YOUR_API_KEY"

[models."api-provider/api-model"]
provider = "api-provider"
model = "api-model"
max_context_size = 262144
`,
);
const fetchMock = vi.fn<FetchMock>(
async (_input, _init) =>
new Response(
Expand Down Expand Up @@ -258,6 +273,10 @@ oauth = { storage = "file", key = "${oauthKey}", oauth_host = "https://auth.dev.
apiKey: '',
oauth: { storage: 'file', key: 'oauth/kimi-code' },
});
expect(config.providers['api-provider']).toMatchObject({ apiKey: 'YOUR_API_KEY' });
expect(config.models?.['api-provider/api-model']).toMatchObject({
provider: 'api-provider',
});
expect(config.services?.moonshotSearch?.oauth).toEqual({
storage: 'file',
key: 'oauth/kimi-code',
Expand Down
19 changes: 17 additions & 2 deletions packages/oauth/src/managed-kimi-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export interface ManagedKimiConfigAdapter<TConfig> {
readonly oauthKey?: string | undefined;
readonly oauthHost?: string | undefined;
readonly preserveDefaultModel?: boolean | undefined;
readonly preserveNonManagedDefaultModel?: boolean;
},
): ManagedKimiCodeApplyResult;
remove?(config: TConfig): void;
Expand All @@ -213,6 +214,7 @@ export interface ProvisionManagedKimiCodeConfigOptions<TConfig> {
readonly oauthKey?: string | undefined;
readonly oauthHost?: string | undefined;
readonly preserveDefaultModel?: boolean | undefined;
readonly preserveNonManagedDefaultModel?: boolean;
readonly fetchImpl?: typeof fetch | undefined;
readonly headers?: Record<string, string> | undefined;
}
Expand Down Expand Up @@ -568,6 +570,7 @@ export function applyManagedKimiCodeConfig(
readonly oauthKey?: string | undefined;
readonly oauthHost?: string | undefined;
readonly preserveDefaultModel?: boolean | undefined;
readonly preserveNonManagedDefaultModel?: boolean;
},
): ManagedKimiCodeApplyResult {
if (options.models.length === 0) {
Expand All @@ -585,6 +588,7 @@ export function applyManagedKimiCodeConfig(
const existingModels = config.models ?? {};
const selectedDefault = selectDefaultModel(config, options.models, {
preserveExisting: options.preserveDefaultModel === true,
preserveNonManaged: options.preserveNonManagedDefaultModel !== false,
});

config.providers[KIMI_CODE_PROVIDER_NAME] = {
Expand Down Expand Up @@ -734,7 +738,10 @@ function forcedThinking(
function selectDefaultModel(
config: ManagedKimiConfigShape,
models: readonly ManagedKimiCodeModelInfo[],
options: { readonly preserveExisting: boolean },
options: {
readonly preserveExisting: boolean;
readonly preserveNonManaged: boolean;
},
): SelectedDefaultModel {
const firstModel = models[0];
if (firstModel === undefined) {
Expand All @@ -751,7 +758,12 @@ function selectDefaultModel(
if (
options.preserveExisting &&
currentDefault !== undefined &&
canPreserveDefaultModel(existingModels, currentDefault, managedModels)
canPreserveDefaultModel(
existingModels,
currentDefault,
managedModels,
options.preserveNonManaged,
)
) {
const preservedModel = managedModels.get(currentDefault);
return {
Expand All @@ -773,8 +785,10 @@ function canPreserveDefaultModel(
existingModels: Record<string, ManagedKimiModelAlias | Record<string, unknown>>,
defaultModel: string,
managedModels: ReadonlyMap<string, ManagedKimiCodeModelInfo>,
preserveNonManaged: boolean,
): boolean {
if (managedModels.has(defaultModel)) return true;
if (!preserveNonManaged) return false;
const existing = existingModels[defaultModel];
return isRecord(existing) && existing['provider'] !== KIMI_CODE_PROVIDER_NAME;
}
Expand Down Expand Up @@ -846,6 +860,7 @@ export async function provisionManagedKimiCodeConfig<TConfig>(
oauthKey: options.oauthKey,
oauthHost: options.oauthHost,
preserveDefaultModel: options.preserveDefaultModel,
preserveNonManagedDefaultModel: options.preserveNonManagedDefaultModel,
});
await options.adapter.write(config);
return {
Expand Down
3 changes: 3 additions & 0 deletions packages/oauth/src/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ export class KimiOAuthToolkit<TConfig = unknown> {
oauthKey,
oauthHost,
preserveDefaultModel: hadToken,
// Re-login keeps an existing managed model selection, but must not
// leave another provider as default after Kimi Code was selected.
preserveNonManagedDefaultModel: name !== KIMI_CODE_PROVIDER_NAME,
fetchImpl: this.fetchImpl,
headers: this.identityHeaders(),
});
Expand Down
1 change: 1 addition & 0 deletions packages/oauth/test/managed-kimi-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ describe('supports_thinking_type', () => {
accessToken: 'oauth-access-token',
fetchImpl: vi.fn(async () => makeThinkingTypeModelsResponse()) as unknown as typeof fetch,
preserveDefaultModel: true,
preserveNonManagedDefaultModel: false,
adapter: {
read: () => config,
write: vi.fn(),
Expand Down
52 changes: 52 additions & 0 deletions packages/oauth/test/toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,58 @@ describe('KimiOAuthToolkit', () => {
expect(write).toHaveBeenCalledWith(config);
});

it.each([
{
description: 'replaces an existing API default during Kimi Code login',
providerName: undefined,
expectedDefaultModel: 'kimi-code/kimi-for-coding',
},
{
description: 'preserves an existing API default during another provider login',
providerName: 'custom-provider',
expectedDefaultModel: 'api-provider/api-model',
},
])('$description', async ({ providerName, expectedDefaultModel }) => {
const storage = new MemoryTokenStorage();
const fetchImpl = vi.fn(async () => managedModelsResponse()) as unknown as typeof fetch;
const config: ManagedKimiConfigShape = {
providers: {
'api-provider': {
type: 'kimi',
apiKey: 'YOUR_API_KEY',
},
},
defaultModel: 'api-provider/api-model',
models: {
'api-provider/api-model': {
provider: 'api-provider',
model: 'api-model',
maxContextSize: 262144,
},
},
};
const toolkit = new KimiOAuthToolkit({
homeDir: join('/tmp', 'kimi-oauth-toolkit-test'),
identity: TEST_IDENTITY,
storage,
now: () => 100,
fetchImpl,
configAdapter: {
read: () => config,
write: vi.fn(),
apply: applyManagedKimiCodeConfig,
},
});

storage.tokens.set('kimi-code', token('access-1'));
await expect(toolkit.login(providerName)).resolves.toMatchObject({
provision: {
defaultModel: expectedDefaultModel,
},
});
expect(config.defaultModel).toBe(expectedDefaultModel);
});

it.each([401, 402])(
'force-refreshes a stored token when managed model provisioning rejects cached auth with HTTP %i',
async (status) => {
Expand Down