Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/chatgpt-oauth-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add ChatGPT Plus/Pro OAuth as a login provider. Run /login and choose OpenAI ChatGPT Plus/Pro to set it up.
141 changes: 140 additions & 1 deletion apps/kimi-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { join } from 'node:path';

import {
applyOpenAICodexConfig,
applyOpenPlatformConfig,
FileTokenStorage,
fetchOpenAICodexModels,
fetchOpenPlatformModels,
filterModelsByPrefix,
getOpenPlatformById,
loginOpenAICodexDeviceCode,
OPENAI_CODEX_MODELS,
OPENAI_CODEX_OAUTH_KEY,
OPENAI_CODEX_PROVIDER_NAME,
OpenPlatformApiError,
resolveKimiTokenStorageName,
type ManagedKimiCodeModelInfo,
type ManagedKimiConfigShape,
type OpenAICodexModelInfo,
type OpenPlatformDefinition,
} from '@moonshot-ai/kimi-code-oauth';
import { log } from '@moonshot-ai/kimi-code-sdk';
import { log, type ModelAlias } from '@moonshot-ai/kimi-code-sdk';

import type { ChoiceOption } from '../components/dialogs/choice-picker';
import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '../constant/kimi-tui';
Expand All @@ -19,6 +30,7 @@ import {
promptLogoutProviderSelection,
promptModelSelectionForOpenPlatform,
promptPlatformSelection,
runModelSelector,
} from './prompts';
import type { SlashCommandHost } from './dispatch';

Expand All @@ -34,12 +46,131 @@ export async function handleLoginCommand(host: SlashCommandHost): Promise<void>
await handleKimiCodeOAuthLogin(host);
return;
}
if (platformId === OPENAI_CODEX_PROVIDER_NAME) {
await handleOpenAICodexOAuthLogin(host);
return;
}

const platform = getOpenPlatformById(platformId);
if (platform === undefined) return;
await handleOpenPlatformLogin(host, platform);
}

async function handleOpenAICodexOAuthLogin(host: SlashCommandHost): Promise<void> {
let spinner: LoginProgressSpinnerHandle | undefined;
const tokenStorage = new FileTokenStorage(join(host.harness.homeDir, 'credentials'));
const tokenStorageName = resolveKimiTokenStorageName({
providerName: OPENAI_CODEX_PROVIDER_NAME,
oauthKey: OPENAI_CODEX_OAUTH_KEY,
});
const controller = new AbortController();
const cancelLogin = (): void => {
controller.abort();
};
host.cancelInFlight = cancelLogin;
let openAICodexModels: readonly OpenAICodexModelInfo[] = OPENAI_CODEX_MODELS;
try {
const token = await loginOpenAICodexDeviceCode({
signal: controller.signal,
onDeviceCode: (data) => {
spinner = host.showLoginAuthorizationPrompt(
{
userCode: data.userCode,
deviceCode: '',
verificationUri: data.verificationUri,
verificationUriComplete: data.verificationUri,
expiresIn: data.expiresIn,
interval: data.interval,
},
{ title: 'Sign in to ChatGPT' },
);
},
});
await tokenStorage.save(tokenStorageName, token);
try {
openAICodexModels = await fetchOpenAICodexModels(token.accessToken, {
signal: controller.signal,
});
} catch (error) {
log.warn('failed to fetch OpenAI Codex models; using built-in fallback', {
providerName: OPENAI_CODEX_PROVIDER_NAME,
sessionId: host.session?.id,
error,
});
}
spinner?.stop({ ok: true, label: 'Logged in.' });
spinner = undefined;
} catch (error) {
const cancelled = controller.signal.aborted;
spinner?.stop({
ok: false,
label: cancelled ? 'Login cancelled.' : 'Login failed.',
});
spinner = undefined;
if (!cancelled) {
log.warn('login failed', {
providerName: OPENAI_CODEX_PROVIDER_NAME,
sessionId: host.session?.id,
error,
});
host.showError(`Login failed: ${formatErrorMessage(error)}`);
}
return;
} finally {
if (host.cancelInFlight === cancelLogin) {
host.cancelInFlight = undefined;
}
}

const modelDict: Record<string, ModelAlias> = {};
for (const model of openAICodexModels) {
const supportEfforts = [...(model.supportEfforts ?? ['low', 'medium', 'high', 'xhigh'])];
modelDict[`${OPENAI_CODEX_PROVIDER_NAME}/${model.id}`] = {
provider: OPENAI_CODEX_PROVIDER_NAME,
model: model.id,
maxContextSize: model.contextLength,
capabilities: model.supportsImageIn
? ['tool_use', 'thinking', 'image_in']
: ['tool_use', 'thinking'],
displayName: model.displayName,
supportEfforts,
defaultEffort: model.defaultEffort ?? (supportEfforts.includes('medium') ? 'medium' : supportEfforts[0]),
};
}
const selection = await runModelSelector(host, modelDict);
if (selection === undefined) {
await tokenStorage.remove(tokenStorageName);
return;
}

const selectedModelId = selection.alias.slice(`${OPENAI_CODEX_PROVIDER_NAME}/`.length);
const existingConfig = await host.harness.getConfig();
if (existingConfig.providers[OPENAI_CODEX_PROVIDER_NAME] !== undefined) {
await host.harness.removeProvider(OPENAI_CODEX_PROVIDER_NAME);
}

const config = await host.harness.getConfig();
applyOpenAICodexConfig(config as ManagedKimiConfigShape, {
selectedModelId,
thinking: selection.thinking !== 'off',
effort:
selection.thinking !== 'off' && selection.thinking !== 'on'
? selection.thinking
: undefined,
models: openAICodexModels,
});
await host.harness.setConfig({
providers: config.providers,
models: config.models,
defaultModel: config.defaultModel,
thinking: config.thinking,
});

await host.authFlow.refreshConfigAfterLogin();
host.track('login', { provider: OPENAI_CODEX_PROVIDER_NAME, method: 'oauth' });
host.showStatus(`Setup complete: OpenAI ChatGPT Plus/Pro · ${selectedModelId}`);
}

async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise<void> {
const status = await host.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME);
const alreadyLoggedIn = status.providers.some(
Expand Down Expand Up @@ -222,6 +353,14 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise<void>
if (target === DEFAULT_OAUTH_PROVIDER_NAME) {
await host.harness.auth.logout(DEFAULT_OAUTH_PROVIDER_NAME);
} else {
if (target === OPENAI_CODEX_PROVIDER_NAME) {
await new FileTokenStorage(join(host.harness.homeDir, 'credentials')).remove(
resolveKimiTokenStorageName({
providerName: OPENAI_CODEX_PROVIDER_NAME,
oauthKey: OPENAI_CODEX_OAUTH_KEY,
}),
);
}
await host.harness.removeProvider(target);
}

Expand Down
5 changes: 4 additions & 1 deletion apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ export interface SlashCommandHost {

// UI
showLoginProgressSpinner(label: string): LoginProgressSpinnerHandle;
showLoginAuthorizationPrompt(auth: DeviceAuthorization): LoginProgressSpinnerHandle;
showLoginAuthorizationPrompt(
auth: DeviceAuthorization,
options?: { readonly title?: string | undefined },
): LoginProgressSpinnerHandle;
showProgressSpinner(label: string): LoginProgressSpinnerHandle;

// Theme
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { OPEN_PLATFORMS } from '@moonshot-ai/kimi-code-oauth';
import { OPENAI_CODEX_PROVIDER_NAME, OPEN_PLATFORMS } from '@moonshot-ai/kimi-code-oauth';

import { ChoicePickerComponent, type ChoiceOption } from './choice-picker';

const PLATFORM_OPTIONS: readonly ChoiceOption[] = [
{ value: 'kimi-code', label: 'Kimi Code (OAuth)' },
{ value: OPENAI_CODEX_PROVIDER_NAME, label: 'OpenAI ChatGPT Plus/Pro (OAuth)' },
...OPEN_PLATFORMS.map((platform) => ({ value: platform.id, label: platform.name })),
];

Expand Down
7 changes: 5 additions & 2 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2183,11 +2183,14 @@ export class KimiTUI {
};
}

showLoginAuthorizationPrompt(auth: DeviceAuthorization): LoginProgressSpinnerHandle {
showLoginAuthorizationPrompt(
auth: DeviceAuthorization,
options: { readonly title?: string | undefined } = {},
): LoginProgressSpinnerHandle {
openUrl(auth.verificationUriComplete);
this.state.transcriptContainer.addChild(
new DeviceCodeBoxComponent({
title: 'Sign in to Kimi Code',
title: options.title ?? 'Sign in to Kimi Code',
url: auth.verificationUriComplete,
code: auth.userCode,
hint: 'Press Ctrl-C to cancel',
Expand Down
4 changes: 2 additions & 2 deletions docs/en/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Two paths when adding:
- **Custom registry (api.json)**: paste a custom registry URL and Bearer token; the CLI automatically creates the `providers` / `models` entries. On later startup, providers from the same registry URL are refreshed together, so upstream provider additions, removals, and model metadata changes are synced.

::: warning
Kimi Code OAuth managed accounts logged in via `/login` do not appear in `/provider`. Use `/login` and `/logout` to manage them.
OAuth accounts logged in via `/login`, including Kimi Code and ChatGPT Plus/Pro, do not appear in `/provider`. Use `/login` and `/logout` to manage them.
:::

The same operations are also available in non-interactive environments via the shell command: [`kimi provider`](../reference/kimi-command.md#kimi-provider).
Expand Down Expand Up @@ -154,7 +154,7 @@ To route Vertex requests through a custom (e.g. proxied) endpoint, set `base_url

## OAuth and credential injection

The Kimi Code managed service uses OAuth rather than static API keys. After running `/login`, the built-in authentication toolchain automatically writes and refreshes credentials — no manual configuration is needed in `config.toml` for this.
The Kimi Code managed service and ChatGPT Plus/Pro provider use OAuth rather than static API keys. After running `/login`, the built-in authentication toolchain automatically writes and refreshes credentials — no manual configuration is needed in `config.toml` for this.

## Next steps

Expand Down
3 changes: 2 additions & 1 deletion docs/en/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ On first launch you need to configure an API source. In the interactive UI, ente
/login
```

`/login` opens a platform selector supporting two options:
`/login` opens a platform selector supporting these options:

- **Kimi Code (OAuth)** — device-code flow; open the link on any device, sign in, and enter the code to authorize
- **OpenAI ChatGPT Plus/Pro (OAuth)** — sign in with a ChatGPT subscription and choose one of the bundled Codex models
- **Kimi Platform API key** — enter an API key from `platform.kimi.com` or `platform.kimi.ai`

To sign out, enter `/logout` to clear the current credentials.
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Kimi Code CLI 支持同时接入多家 LLM 平台——用 Kimi Code 托管服
- **Custom registry (api.json)**:粘贴自定义 registry 地址和 Bearer token,CLI 自动创建 `providers` / `models` 条目。后续启动时,同一个 registry 地址下的供应商会一起刷新,因此上游新增、删除供应商以及模型元数据变化都会同步。

::: warning
通过 `/login` 登录的 Kimi Code OAuth 托管账号不会在 `/provider` 里显示,请用 `/login` 和 `/logout` 管理。
通过 `/login` 登录的 OAuth 账号(包括 Kimi Code 和 ChatGPT Plus/Pro)不会在 `/provider` 里显示,请用 `/login` 和 `/logout` 管理。
:::

非交互环境下也可以用 shell 命令完成同样操作:[`kimi provider`](../reference/kimi-command.md#kimi-provider)。
Expand Down Expand Up @@ -154,7 +154,7 @@ kimi

## OAuth 与凭证注入

Kimi Code 托管服务使用 OAuth 而非静态 API 密钥。运行 `/login` 后,内置的认证工具链会自动写入并刷新凭证,`config.toml` 里无需手动配置这部分内容。
Kimi Code 托管服务和 ChatGPT Plus/Pro 供应商使用 OAuth 而非静态 API 密钥。运行 `/login` 后,内置的认证工具链会自动写入并刷新凭证,`config.toml` 里无需手动配置这部分内容。

## 下一步

Expand Down
3 changes: 2 additions & 1 deletion docs/zh/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ kimi -c
/login
```

`/login` 会弹出平台选择器,支持两种方式
`/login` 会弹出平台选择器,支持以下方式

- **Kimi Code(OAuth)** — 验证码流程,在任意设备打开链接、登录并输入验证码即可授权
- **OpenAI ChatGPT Plus/Pro(OAuth)** — 使用 ChatGPT 订阅登录,并选择内置的 Codex 模型
- **Kimi Platform API 密钥** — 输入来自 `platform.kimi.com` 或 `platform.kimi.ai` 的 API 密钥

需要退出登录时,输入 `/logout` 清除当前凭证。
Expand Down
10 changes: 6 additions & 4 deletions packages/agent-core/src/agent/compaction/full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,12 @@ export class FullCompaction {
: undefined;
const provider = applyCompletionBudget({
provider: this.agent.config.provider,
budget: resolveCompletionBudget({
maxOutputSize: this.agent.config.maxOutputSize ?? defaultCompactionCap,
reservedContextSize: this.agent.kimiConfig?.loopControl?.reservedContextSize,
}),
budget: this.agent.config.disableCompletionBudget
? undefined
: resolveCompletionBudget({
maxOutputSize: this.agent.config.maxOutputSize ?? defaultCompactionCap,
reservedContextSize: this.agent.kimiConfig?.loopControl?.reservedContextSize,
}),
capability,
});
const instruction = this.buildInstruction(data.instruction);
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-core/src/agent/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ export class ConfigState {
return this.tryResolvedProviderConfig()?.maxOutputSize;
}

get disableCompletionBudget(): boolean {
return this.tryResolvedProviderConfig()?.disableCompletionBudget === true;
}

private get resolvedProviderConfig(): ResolvedRuntimeProvider | undefined {
if (this._modelAlias === undefined) return undefined;
return this.agent.modelProvider?.resolveProviderConfig(this._modelAlias);
Expand Down
10 changes: 6 additions & 4 deletions packages/agent-core/src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,12 @@ export class Agent {
// is applied in ConfigState.provider so compaction shares it. See get provider().
const provider = this.config.provider;
const loopControl = this.kimiConfig?.loopControl;
const completionBudgetConfig = resolveCompletionBudget({
maxOutputSize: this.config.maxOutputSize,
reservedContextSize: loopControl?.reservedContextSize,
});
const completionBudgetConfig = this.config.disableCompletionBudget
? undefined
: resolveCompletionBudget({
maxOutputSize: this.config.maxOutputSize,
reservedContextSize: loopControl?.reservedContextSize,
});
return new KosongLLM({
provider,
systemPrompt: this.config.systemPrompt,
Expand Down
Loading