feat: add Kimi Code OAuth support (import from Kimi CLI)
Add native Kimi Code OAuth support, allowing users to authenticate with
their Kimi subscription instead of requiring a static API key.
Changes:
- New kimi-code-oauth.ts: token refresh via auth.kimi.com/api/oauth/token
- Auth dispatch: route kimi-code provider to custom refresh (same pattern
as qwen-portal and chutes)
- New 'kimi-code-oauth' onboarding auth choice: imports OAuth credentials
from Kimi CLI (~/.kimi/credentials/kimi-code.json)
- Handles expires_at conversion (Kimi uses seconds, Moltbot uses ms)
Usage:
moltbot onboard --auth-choice kimi-code-oauth
Requires Kimi CLI to be logged in first (kimi login).
🤖 AI-assisted (Claude Opus 4.5 + Kimi K2.5 sub-agent)
Testing: endpoint + refresh verified via curl; compiles clean; lint clean; not yet tested e2e through Moltbot
This commit is contained in:
parent
6044bf3637
commit
425c9a941a
@ -3,6 +3,7 @@ import lockfile from "proper-lockfile";
|
||||
|
||||
import type { MoltbotConfig } from "../../config/config.js";
|
||||
import { refreshChutesTokens } from "../chutes-oauth.js";
|
||||
import { refreshKimiCodeCredentials } from "../../providers/kimi-code-oauth.js";
|
||||
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
@ -62,7 +63,12 @@ async function refreshOAuthTokenWithLock(params: {
|
||||
const newCredentials = await refreshQwenPortalCredentials(cred);
|
||||
return { apiKey: newCredentials.access, newCredentials };
|
||||
})()
|
||||
: await getOAuthApiKey(cred.provider as OAuthProvider, oauthCreds);
|
||||
: String(cred.provider) === "kimi-code"
|
||||
? await (async () => {
|
||||
const newCredentials = await refreshKimiCodeCredentials(cred);
|
||||
return { apiKey: newCredentials.access, newCredentials };
|
||||
})()
|
||||
: await getOAuthApiKey(cred.provider as OAuthProvider, oauthCreds);
|
||||
if (!result) return null;
|
||||
store.profiles[params.profileId] = {
|
||||
...cred,
|
||||
|
||||
@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||
.option(
|
||||
"--auth-choice <choice>",
|
||||
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|kimi-code-oauth|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||
)
|
||||
.option(
|
||||
"--token-provider <id>",
|
||||
|
||||
@ -99,7 +99,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||
value: "moonshot",
|
||||
label: "Moonshot AI",
|
||||
hint: "Kimi K2 + Kimi Code",
|
||||
choices: ["moonshot-api-key", "kimi-code-api-key"],
|
||||
choices: ["moonshot-api-key", "kimi-code-api-key", "kimi-code-oauth"],
|
||||
},
|
||||
{
|
||||
value: "zai",
|
||||
@ -141,6 +141,11 @@ export function buildAuthChoiceOptions(params: {
|
||||
});
|
||||
options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" });
|
||||
options.push({ value: "kimi-code-api-key", label: "Kimi Code API key" });
|
||||
options.push({
|
||||
value: "kimi-code-oauth",
|
||||
label: "Kimi Code OAuth (import from Kimi CLI)",
|
||||
hint: "Reads credentials from ~/.kimi/credentials/",
|
||||
});
|
||||
options.push({ value: "synthetic-api-key", label: "Synthetic API key" });
|
||||
options.push({
|
||||
value: "venice-api-key",
|
||||
|
||||
@ -14,6 +14,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"ai-gateway-api-key": "vercel-ai-gateway",
|
||||
"moonshot-api-key": "moonshot",
|
||||
"kimi-code-api-key": "kimi-code",
|
||||
"kimi-code-oauth": "kimi-code",
|
||||
"gemini-api-key": "google",
|
||||
"google-antigravity": "google-antigravity",
|
||||
"google-gemini-cli": "google-gemini-cli",
|
||||
|
||||
@ -252,6 +252,52 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return applyMoonshotConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (authChoice === "kimi-code-oauth") {
|
||||
const os = await import("node:os");
|
||||
const path = await import("node:path");
|
||||
const fs = await import("node:fs");
|
||||
const kimiCredPath = path.join(os.homedir(), ".kimi", "credentials", "kimi-code.json");
|
||||
if (!fs.existsSync(kimiCredPath)) {
|
||||
runtime.error(
|
||||
`Kimi CLI credentials not found at ${kimiCredPath}.\n` +
|
||||
"Please log in with Kimi CLI first: kimi login",
|
||||
);
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
let raw: { access_token?: string; refresh_token?: string; expires_at?: number };
|
||||
try {
|
||||
raw = JSON.parse(fs.readFileSync(kimiCredPath, "utf-8"));
|
||||
} catch {
|
||||
runtime.error(`Failed to parse Kimi CLI credentials at ${kimiCredPath}.`);
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
if (!raw.access_token || !raw.refresh_token) {
|
||||
runtime.error("Kimi CLI credentials file is missing access_token or refresh_token.");
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
upsertAuthProfile({
|
||||
profileId: "kimi-code:default",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "kimi-code",
|
||||
access: raw.access_token,
|
||||
refresh: raw.refresh_token,
|
||||
// Kimi stores expires_at in seconds; Moltbot uses milliseconds
|
||||
expires: raw.expires_at ? raw.expires_at * 1000 : 0,
|
||||
},
|
||||
});
|
||||
runtime.log("Imported Kimi Code OAuth credentials from Kimi CLI.");
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "kimi-code:default",
|
||||
provider: "kimi-code",
|
||||
mode: "oauth",
|
||||
});
|
||||
return applyKimiCodeConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (authChoice === "kimi-code-api-key") {
|
||||
const resolved = await resolveNonInteractiveApiKey({
|
||||
provider: "kimi-code",
|
||||
|
||||
@ -15,6 +15,7 @@ export type AuthChoice =
|
||||
| "ai-gateway-api-key"
|
||||
| "moonshot-api-key"
|
||||
| "kimi-code-api-key"
|
||||
| "kimi-code-oauth"
|
||||
| "synthetic-api-key"
|
||||
| "venice-api-key"
|
||||
| "codex-cli"
|
||||
|
||||
55
src/providers/kimi-code-oauth.ts
Normal file
55
src/providers/kimi-code-oauth.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
|
||||
const KIMI_CODE_OAUTH_HOST = "https://auth.kimi.com";
|
||||
const KIMI_CODE_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
||||
|
||||
export async function refreshKimiCodeCredentials(
|
||||
credentials: OAuthCredentials,
|
||||
): Promise<OAuthCredentials> {
|
||||
if (!credentials.refresh?.trim()) {
|
||||
throw new Error("Kimi Code OAuth refresh token missing; re-authenticate.");
|
||||
}
|
||||
|
||||
const host = process.env.KIMI_CODE_OAUTH_HOST || KIMI_CODE_OAUTH_HOST;
|
||||
|
||||
const response = await fetch(`${host}/api/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: credentials.refresh,
|
||||
client_id: KIMI_CODE_CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error(
|
||||
`Kimi Code OAuth refresh token expired or revoked. Re-authenticate with ` +
|
||||
`\`moltbot onboard --auth-choice kimi-code-oauth\`.`,
|
||||
);
|
||||
}
|
||||
throw new Error(`Kimi Code OAuth refresh failed: ${text || response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
access_token?: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
};
|
||||
|
||||
if (!payload.access_token || !payload.expires_in) {
|
||||
throw new Error("Kimi Code OAuth refresh response missing access token.");
|
||||
}
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
access: payload.access_token,
|
||||
refresh: payload.refresh_token || credentials.refresh,
|
||||
expires: Date.now() + payload.expires_in * 1000,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user