feat(kimi): merge OAuth into dual-auth provider, add K3/K2.7 models

Gộp kimi-coding vào kimi (oauth+apikey), parity CLIProxyAPI device flow/headers/refresh.
Thêm K3 + K2.7 Code (+ Kimi Code ids), pricing/caps vision, cập nhật baseline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-07-17 12:09:14 +07:00
co-authored by Cursor
parent bc252ea802
commit 68566f53dc
21 changed files with 332 additions and 228 deletions
+1 -1
View File
@@ -131,7 +131,7 @@ const APIKEY_PROVIDERS = {
openrouter: { id: "openrouter", name: "OpenRouter" },
glm: { id: "glm", name: "GLM Coding" },
minimax: { id: "minimax", name: "Minimax Coding" },
kimi: { id: "kimi", name: "Kimi Coding" },
kimi: { id: "kimi", name: "Kimi" },
openai: { id: "openai", name: "OpenAI" },
anthropic: { id: "anthropic", name: "Anthropic" },
gemini: { id: "gemini", name: "Gemini" },
+39 -6
View File
@@ -1,6 +1,7 @@
import { platform, arch } from "os";
import { platform, arch, hostname } from "os";
import { PROVIDERS, PROVIDER_OAUTH } from "./providers.js";
import { ANTIGRAVITY_IDE_USER_AGENT } from "../providers/shared.js";
import { createRequire } from "module";
// === Gemini CLI === derive từ registry gemini-cli.transport
export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion;
@@ -171,12 +172,44 @@ export const OAUTH_ENDPOINTS = {
github: { token: PROVIDER_OAUTH["github"]?.tokenUrl, auth: PROVIDER_OAUTH["github"]?.authorizeUrl, deviceCode: PROVIDER_OAUTH["github"]?.deviceCodeUrl },
};
// Generate Kimi OAuth custom headers
export function buildKimiHeaders() {
let _appVersion;
function getAppPackageVersion() {
if (_appVersion) return _appVersion;
try {
const require = createRequire(import.meta.url);
_appVersion = require("../../package.json").version || "0.0.0";
} catch {
_appVersion = process.env.npm_package_version || "0.0.0";
}
return _appVersion;
}
// Kimi Code OAuth / API headers (CLIProxyAPI internal/auth/kimi commonHeaders parity).
// deviceId must stay stable per connection for the whole OAuth session.
export function buildKimiHeaders(deviceId) {
const osName = platform();
const architecture = arch();
let deviceModel = `${osName} ${architecture}`;
if (osName === "darwin") deviceModel = `macOS ${architecture}`;
else if (osName === "win32") deviceModel = `Windows ${architecture}`;
else if (osName === "linux") deviceModel = `Linux ${architecture}`;
let deviceName = "unknown";
try {
deviceName = hostname() || "unknown";
} catch {
deviceName = "unknown";
}
const resolvedId = (typeof deviceId === "string" && deviceId.trim())
? deviceId.trim()
: `kimi-${Date.now()}`;
return {
"X-Msh-Platform": "9router",
"X-Msh-Version": "2.1.2",
"X-Msh-Device-Model": typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown",
"X-Msh-Device-Id": `kimi-${Date.now()}`
"X-Msh-Version": getAppPackageVersion(),
"X-Msh-Device-Name": deviceName,
"X-Msh-Device-Model": deviceModel,
"X-Msh-Device-Id": resolvedId,
};
}
+12 -6
View File
@@ -38,7 +38,8 @@ function applyAuth(headers, desc, credentials) {
// Provider-specific header quirks kept as small hooks (not pure auth).
const HEADER_HOOKS = {
kimiHeaders: (h) => Object.assign(h, buildKimiHeaders()),
// Stable device_id from OAuth connection (CLIProxyAPI KimiTokenStorage.DeviceID)
kimiHeaders: (h, c) => Object.assign(h, buildKimiHeaders(c?.providerSpecificData?.deviceId)),
clineHeaders: (h, c) => Object.assign(h, buildClineHeaders(c.apiKey || c.accessToken)),
kilocodeOrg: (h, c) => { if (c.providerSpecificData?.orgId) h["X-Kilocode-OrganizationID"] = c.providerSpecificData.orgId; },
claudeOverlay: (h) => {
@@ -227,7 +228,8 @@ export class DefaultExecutor extends BaseExecutor {
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
clinepass: () => this.refreshCline(credentials.refreshToken, proxyOptions),
"kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken, proxyOptions),
kimi: () => this.refreshKimi(credentials, proxyOptions),
"kimi-coding": () => this.refreshKimi(credentials, proxyOptions),
kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions)
};
@@ -307,16 +309,20 @@ export class DefaultExecutor extends BaseExecutor {
return { accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
}
async refreshKimiCoding(refreshToken, proxyOptions = null) {
const kimiHeaders = buildKimiHeaders();
const response = await proxyAwareFetch(PROVIDERS["kimi-coding"].refreshUrl, {
// CLIProxyAPI DeviceFlowClient.RefreshToken — form body + X-Msh-* headers + stable device_id
async refreshKimi(credentials, proxyOptions = null) {
const refreshToken = credentials.refreshToken;
const cfg = PROVIDERS.kimi || PROVIDERS["kimi-coding"];
if (!cfg?.refreshUrl || !cfg?.clientId) return null;
const kimiHeaders = buildKimiHeaders(credentials?.providerSpecificData?.deviceId);
const response = await proxyAwareFetch(cfg.refreshUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
...kimiHeaders
},
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS["kimi-coding"].clientId })
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: cfg.clientId })
}, proxyOptions);
if (!response.ok) return null;
const tokens = await response.json();
+11 -1
View File
@@ -96,6 +96,14 @@ export const MODEL_CAPABILITIES = {
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
"vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
"coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
// Kimi flagship + coding (platform + Kimi Code ids) — vision/video native
"kimi-k3": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 131072 },
"k3": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 131072 },
"kimi-for-coding": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
"kimi-for-coding-highspeed": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
"kimi-k2.7-code": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
"kimi-k2.7-code-highspeed": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
};
const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 };
@@ -222,7 +230,9 @@ export const PATTERN_CAPABILITIES = [
{ pattern: "*qwen*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
// ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ─────
{ pattern: "*kimi*k2.7*code*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "*kimi*k3*", caps: { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 1048576, maxOutput: 131072 } },
{ pattern: "*kimi*for-coding*", caps: { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 } },
{ pattern: "*kimi*k2.7*code*", caps: { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 } },
{ pattern: "*kimi*k2*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },
+9
View File
@@ -75,10 +75,18 @@ export const MODEL_PRICING = {
"qwen3-coder-flash": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
// === Kimi ===
// Official platform.kimi.ai: cache-hit / cache-miss / output per 1M tokens
"kimi-k3": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.00 },
"k3": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.00 },
"kimi-k2.7-code": { input: 0.95, output: 4.00, cached: 0.19, reasoning: 4.00, cache_creation: 0.95 },
"kimi-k2.7-code-highspeed": { input: 1.90, output: 8.00, cached: 0.38, reasoning: 8.00, cache_creation: 1.90 },
"kimi-for-coding": { input: 0.95, output: 4.00, cached: 0.19, reasoning: 4.00, cache_creation: 0.95 },
"kimi-for-coding-highspeed": { input: 1.90, output: 8.00, cached: 0.38, reasoning: 8.00, cache_creation: 1.90 },
"kimi-k2": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
"kimi-k2-thinking": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 },
"kimi-k2.5": { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 },
"kimi-k2.5-thinking": { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 },
"kimi-k2.6": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
"kimi-latest": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
// === DeepSeek ===
@@ -185,6 +193,7 @@ export const PATTERN_PRICING = [
// --- Kimi ---
{ pattern: "kimi-*-thinking", pricing: { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 } },
{ pattern: "kimi-k3*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.00 } },
{ pattern: "kimi-k2*", pricing: { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 } },
{ pattern: "kimi-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },
+46 -48
View File
@@ -52,53 +52,52 @@ import p49 from "./jina-ai.js";
import p50 from "./jina-reader.js";
import p51 from "./kilocode.js";
import p52 from "./kimchi.js";
import p53 from "./kimi-coding.js";
import p54 from "./kimi.js";
import p55 from "./kiro.js";
import p56 from "./linkup.js";
import p57 from "./local-device.js";
import p58 from "./mimo-free.js";
import p59 from "./minimax-cn.js";
import p60 from "./minimax.js";
import p61 from "./mistral.js";
import p62 from "./mmf.js";
import p63 from "./nanobanana.js";
import p64 from "./nebius.js";
import p65 from "./nvidia.js";
import p66 from "./ollama-local.js";
import p67 from "./ollama.js";
import p68 from "./openai.js";
import p69 from "./opencode-go.js";
import p70 from "./opencode.js";
import p71 from "./openrouter.js";
import p72 from "./perplexity-web.js";
import p73 from "./perplexity.js";
import p74 from "./perplexity-agent.js";
import p75 from "./playht.js";
import p76 from "./qoder.js";
import p77 from "./qwen.js";
import p78 from "./recraft.js";
import p79 from "./runwayml.js";
import p80 from "./sdwebui.js";
import p81 from "./searchapi.js";
import p82 from "./searxng.js";
import p83 from "./serper.js";
import p84 from "./siliconflow.js";
import p85 from "./stability-ai.js";
import p86 from "./tavily.js";
import p87 from "./together.js";
import p88 from "./topaz.js";
import p89 from "./tortoise.js";
import p90 from "./venice.js";
import p91 from "./vercel-ai-gateway.js";
import p92 from "./vertex-partner.js";
import p93 from "./vertex.js";
import p94 from "./volcengine-ark.js";
import p95 from "./voyage-ai.js";
import p96 from "./xai.js";
import p97 from "./xiaomi-mimo.js";
import p98 from "./xiaomi-tokenplan.js";
import p99 from "./youcom.js";
import p53 from "./kimi.js";
import p54 from "./kiro.js";
import p55 from "./linkup.js";
import p56 from "./local-device.js";
import p57 from "./mimo-free.js";
import p58 from "./minimax-cn.js";
import p59 from "./minimax.js";
import p60 from "./mistral.js";
import p61 from "./mmf.js";
import p62 from "./nanobanana.js";
import p63 from "./nebius.js";
import p64 from "./nvidia.js";
import p65 from "./ollama-local.js";
import p66 from "./ollama.js";
import p67 from "./openai.js";
import p68 from "./opencode-go.js";
import p69 from "./opencode.js";
import p70 from "./openrouter.js";
import p71 from "./perplexity-web.js";
import p72 from "./perplexity.js";
import p73 from "./perplexity-agent.js";
import p74 from "./playht.js";
import p75 from "./qoder.js";
import p76 from "./qwen.js";
import p77 from "./recraft.js";
import p78 from "./runwayml.js";
import p79 from "./sdwebui.js";
import p80 from "./searchapi.js";
import p81 from "./searxng.js";
import p82 from "./serper.js";
import p83 from "./siliconflow.js";
import p84 from "./stability-ai.js";
import p85 from "./tavily.js";
import p86 from "./together.js";
import p87 from "./topaz.js";
import p88 from "./tortoise.js";
import p89 from "./venice.js";
import p90 from "./vercel-ai-gateway.js";
import p91 from "./vertex-partner.js";
import p92 from "./vertex.js";
import p93 from "./volcengine-ark.js";
import p94 from "./voyage-ai.js";
import p95 from "./xai.js";
import p96 from "./xiaomi-mimo.js";
import p97 from "./xiaomi-tokenplan.js";
import p98 from "./youcom.js";
export default [
p0,
@@ -200,5 +199,4 @@ export default [
p96,
p97,
p98,
p99
];
@@ -1,65 +0,0 @@
import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js";
export default {
id: "kimi-coding",
hidden: true,
priority: 120,
alias: "kmc",
display: {
name: "Kimi Coding",
icon: "psychology",
color: "#1E40AF",
textIcon: "KC",
website: "https://kimi.moonshot.cn",
notice: {
signupUrl: "https://kimi.moonshot.cn",
},
},
category: "oauth",
transport: {
baseUrl: "https://api.kimi.com/coding/v1/messages",
format: "claude",
urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS },
clientId: "17e5f671-d194-4dfb-9706-5516cb48c098",
tokenUrl: "https://auth.kimi.com/api/oauth/token",
refreshUrl: "https://auth.kimi.com/api/oauth/token",
auth: {
combined: true,
header: "x-api-key",
scheme: "raw",
hooks: [
"kimiHeaders",
],
},
},
// Multi-endpoint: pick the transport matching client sourceFormat to skip translation.
transports: [
{
format: "openai",
baseUrl: "https://api.kimi.com/coding/v1/chat/completions",
auth: { combined: true, header: "Authorization", scheme: "bearer", hooks: ["kimiHeaders"] },
},
{
format: "claude",
baseUrl: "https://api.kimi.com/coding/v1/messages",
urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS },
auth: { combined: true, header: "x-api-key", scheme: "raw", hooks: ["kimiHeaders"] },
},
],
models: [
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" },
],
oauth: {
deviceCodeUrl: "https://auth.kimi.com/api/oauth/device_authorization",
tokenUrl: "https://auth.kimi.com/api/oauth/token",
refreshLeadMs: 300000,
},
features: {
usage: true,
},
};
+40 -7
View File
@@ -1,9 +1,14 @@
import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js";
import { CLAUDE_API_HEADERS } from "../shared.js";
// Dual auth (same pattern as xai): OAuth = Kimi Code subscription (device code),
// API key = platform.moonshot / api.kimi.com. Transport is shared.
// CLIProxyAPI parity: client_id, auth.kimi.com device+token, X-Msh-* headers, device_id.
export default {
id: "kimi",
priority: 170,
alias: "kimi",
// Legacy id + short alias from former kimi-coding registry entry
aliases: ["kimi-coding", "kmc"],
display: {
name: "Kimi",
icon: "psychology",
@@ -12,18 +17,25 @@ export default {
website: "https://kimi.moonshot.cn",
notice: {
apiKeyUrl: "https://platform.moonshot.ai/console/api-keys",
signupUrl: "https://www.kimi.com/code",
},
},
category: "apikey",
category: "oauth",
authModes: ["oauth", "apikey"],
hasOAuth: true,
transport: {
baseUrl: "https://api.kimi.com/coding/v1/messages",
format: "claude",
urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS },
clientId: "17e5f671-d194-4dfb-9706-5516cb48c098",
tokenUrl: "https://auth.kimi.com/api/oauth/token",
refreshUrl: "https://auth.kimi.com/api/oauth/token",
auth: {
combined: true,
header: "x-api-key",
scheme: "raw",
hooks: ["kimiHeaders"],
},
},
// Multi-endpoint: pick the transport matching client sourceFormat to skip translation.
@@ -31,26 +43,47 @@ export default {
{
format: "openai",
baseUrl: "https://api.kimi.com/coding/v1/chat/completions",
auth: { combined: true, header: "Authorization", scheme: "bearer" },
auth: { combined: true, header: "Authorization", scheme: "bearer", hooks: ["kimiHeaders"] },
},
{
format: "claude",
baseUrl: "https://api.kimi.com/coding/v1/messages",
urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS },
auth: { combined: true, header: "x-api-key", scheme: "raw" },
auth: { combined: true, header: "x-api-key", scheme: "raw", hooks: ["kimiHeaders"] },
},
],
models: [
// Flagship K3 — platform.kimi.ai id `kimi-k3`, Kimi Code OAuth id `k3` (up to 1M)
{ id: "kimi-k3", name: "Kimi K3" },
{ id: "k3", name: "Kimi K3 (Code)" },
// Kimi Code subscription stable ids (map to K2.7 Code backend)
{ id: "kimi-for-coding", name: "Kimi for Coding" },
{ id: "kimi-for-coding-highspeed", name: "Kimi for Coding Highspeed" },
// Pay-as-you-go platform ids
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
{ id: "kimi-k2.7-code-highspeed", name: "Kimi K2.7 Code Highspeed" },
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" },
],
serviceKinds: ["llm","webSearch"],
serviceKinds: ["llm", "webSearch"],
searchViaChat: {
defaultModel: "kimi-k2.5",
defaultModel: "kimi-k3",
endpoint: "https://api.moonshot.cn/v1/chat/completions",
pricingUrl: "https://platform.moonshot.ai/docs/pricing/chat",
pricingUrl: "https://platform.kimi.ai/docs/pricing/chat",
},
oauth: {
clientId: "17e5f671-d194-4dfb-9706-5516cb48c098",
deviceCodeUrl: "https://auth.kimi.com/api/oauth/device_authorization",
tokenUrl: "https://auth.kimi.com/api/oauth/token",
refreshUrl: "https://auth.kimi.com/api/oauth/token",
// CLIProxyAPI refreshThresholdSeconds = 300
refreshLeadMs: 300000,
authorizeDeviceUrl: "https://www.kimi.com/code/authorize_device",
},
features: {
usage: true,
},
};
+9 -1
View File
@@ -3,6 +3,7 @@ import { OAUTH_ENDPOINTS, REFRESH_LEAD_MS } from "../config/appConstants.js";
import {
refreshXaiToken,
refreshAccessToken,
refreshKimiToken,
refreshClaudeOAuthToken,
refreshGoogleToken,
refreshQwenToken,
@@ -18,6 +19,7 @@ import {
// Re-export all provider refresh functions (preserves public API for all consumers)
export {
refreshAccessToken,
refreshKimiToken,
refreshClaudeOAuthToken,
refreshGoogleToken,
refreshQwenToken,
@@ -44,7 +46,10 @@ export function isUnrecoverableRefreshError(result) {
}
export function getRefreshLeadMs(provider) {
return REFRESH_LEAD_MS[provider] || TOKEN_EXPIRY_BUFFER_MS;
if (REFRESH_LEAD_MS[provider]) return REFRESH_LEAD_MS[provider];
// Legacy id after kimi-coding → kimi merge
if (provider === "kimi-coding" && REFRESH_LEAD_MS.kimi) return REFRESH_LEAD_MS.kimi;
return TOKEN_EXPIRY_BUFFER_MS;
}
export function parseVertexSaJson(apiKey) {
@@ -133,6 +138,9 @@ const REFRESH_HANDLERS = {
"grok-cli": (c, log) => refreshXaiToken(c.refreshToken, log),
gcli: (c, log) => refreshXaiToken(c.refreshToken, log),
"codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log),
// Kimi Code OAuth (merged into id `kimi`); legacy id still routes here
kimi: (c, log) => refreshKimiToken(c.refreshToken, c, log),
"kimi-coding": (c, log) => refreshKimiToken(c.refreshToken, c, log),
vertex: vertexRefreshHandler,
"vertex-partner": vertexRefreshHandler
};
+47 -1
View File
@@ -1,5 +1,5 @@
import { PROVIDERS, PROVIDER_OAUTH } from "../../config/providers.js";
import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../../config/appConstants.js";
import { OAUTH_ENDPOINTS, GITHUB_COPILOT, buildKimiHeaders } from "../../config/appConstants.js";
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { dedupRefresh } from "./dedup.js";
import { buildExternalIdpRefreshParams } from "../../../src/lib/oauth/kiroExternalIdp.js";
@@ -91,6 +91,52 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
}, log);
}
// CLIProxyAPI DeviceFlowClient.RefreshToken: form body (no client_secret) + X-Msh-* headers
export async function refreshKimiToken(refreshToken, credentials, log) {
const config = PROVIDERS.kimi;
if (!config?.refreshUrl || !config?.clientId) {
log?.warn?.("TOKEN_REFRESH", "No Kimi refresh URL/clientId configured");
return null;
}
if (!refreshToken) return null;
return dedupRefresh("kimi", refreshToken, async () => {
try {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
...buildKimiHeaders(credentials?.providerSpecificData?.deviceId),
};
const response = await fetch(config.refreshUrl, {
method: "POST",
headers,
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: config.clientId,
}),
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", `Failed to refresh token for kimi`, {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
} catch (error) {
log?.error?.("TOKEN_REFRESH", `Error refreshing token for kimi`, { error: error.message });
return null;
}
}, log);
}
export async function refreshClaudeOAuthToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("claude", refreshToken, async () => {
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { getProviderIconSrc, markProviderIconMissing } from "@/shared/utils/providerIcon";
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS } from "@/shared/constants/providers";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
@@ -151,8 +152,12 @@ export default function ProviderDetailPage() {
const oauthConnectionLabel =
providerId === "xai" ? "Grok Build OAuth"
: providerId === "grok-cli" ? "Grok CLI Device Login"
: providerId === "kimi" ? "Kimi Coding OAuth"
: "OAuth";
const apiKeyConnectionLabel = providerId === "xai" ? "xAI API Key" : "API Key";
const apiKeyConnectionLabel =
providerId === "xai" ? "xAI API Key"
: providerId === "kimi" ? "Kimi API Key"
: "API Key";
// Resolve suffix "(level)" for a model when a thinking level is picked and the model supports it.
const resolveThinkingSuffix = (modelId) => {
if (!thinkingMode || thinkingMode === "auto") return null;
@@ -1231,7 +1236,7 @@ export default function ProviderDetailPage() {
if (isAnthropicCompatible) {
return "/providers/anthropic-m.png";
}
return `/providers/${providerInfo.id}.png`;
return getProviderIconSrc(providerInfo.id);
};
return (
@@ -1250,7 +1255,7 @@ export default function ProviderDetailPage() {
className="flex size-12 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: `${providerInfo.color}15` }}
>
{headerImgError ? (
{headerImgError || !getHeaderIconPath() ? (
<span className="text-sm font-bold" style={{ color: providerInfo.color }}>
{providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()}
</span>
@@ -1262,7 +1267,12 @@ export default function ProviderDetailPage() {
height={48}
className="max-h-12 max-w-12 rounded-lg object-contain"
sizes="48px"
onError={() => setHeaderImgError(true)}
onError={() => {
markProviderIconMissing(providerInfo.id);
setHeaderImgError(true);
}}
loading="lazy"
decoding="async"
/>
)}
</div>
@@ -154,6 +154,7 @@ export async function GET(request, { params }) {
const noPkceDeviceProviders = [
"github",
"kiro",
"kimi",
"kimi-coding",
"kilocode",
"codebuddy-cn",
@@ -279,10 +280,11 @@ export async function POST(request, { params }) {
}
// Providers that don't use PKCE for device code
const noPkceProviders = ["github", "kimi-coding", "kilocode", "codebuddy-cn"];
const noPkceProviders = ["github", "kimi", "kimi-coding", "kilocode", "codebuddy-cn"];
let result;
if (noPkceProviders.includes(provider)) {
result = await pollForToken(provider, deviceCode);
// kimi needs extraData._kimiDeviceId for stable X-Msh-Device-Id (CLIProxyAPI parity)
result = await pollForToken(provider, deviceCode, null, extraData);
} else if (provider === "kiro") {
// Kiro needs extraData (clientId, clientSecret) from device code response
result = await pollForToken(provider, deviceCode, null, extraData);
@@ -303,9 +305,10 @@ export async function POST(request, { params }) {
}
if (result.success) {
// Save to database
// Save to database (legacy kimi-coding OAuth → dual-auth kimi)
const providerId = provider === "kimi-coding" ? "kimi" : provider;
const connection = await createProviderConnection({
provider,
provider: providerId,
authType: "oauth",
...result.tokens,
expiresAt: result.tokens.expiresIn
+2 -1
View File
@@ -75,7 +75,8 @@ const OAUTH_TEST_CONFIG = {
authPrefix: "Bearer ",
refreshable: false,
},
"kimi-coding": { checkExpiry: true, refreshable: false },
kimi: { checkExpiry: true, refreshable: true },
"kimi-coding": { checkExpiry: true, refreshable: true },
cursor: { tokenExists: true },
kilocode: {
url: `${KILOCODE_CONFIG.apiBaseUrl}/api/profile`,
+13 -6
View File
@@ -89,12 +89,18 @@ export const CURSOR_CONFIG = {
},
};
// Kimi Coding OAuth Configuration (Device Code Flow)
// clientId uses env override — dynamic, not stored in registry
export const KIMI_CODING_CONFIG = {
...PROVIDER_OAUTH["kimi-coding"],
clientId: process.env.KIMI_CODING_OAUTH_CLIENT_ID || REGISTRY_PROVIDERS["kimi-coding"]?.clientId,
// Kimi Code OAuth (Device Code Flow) — merged into provider id `kimi` (dual auth)
// clientId: registry first, env override for forks
export const KIMI_CONFIG = {
...PROVIDER_OAUTH["kimi"],
clientId:
process.env.KIMI_CODING_OAUTH_CLIENT_ID ||
process.env.KIMI_OAUTH_CLIENT_ID ||
REGISTRY_PROVIDERS["kimi"]?.clientId ||
PROVIDER_OAUTH["kimi"]?.clientId,
};
// Back-compat alias for any remaining KIMI_CODING_CONFIG imports
export const KIMI_CODING_CONFIG = KIMI_CONFIG;
// KiloCode OAuth Configuration (Custom Device Auth Flow)
export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] };
@@ -134,7 +140,8 @@ export const PROVIDERS = {
GITHUB: "github",
KIRO: "kiro",
CURSOR: "cursor",
KIMI_CODING: "kimi-coding",
KIMI: "kimi",
KIMI_CODING: "kimi",
KILOCODE: "kilocode",
CLINE: "cline",
CLINEPASS: "clinepass",
+41 -14
View File
@@ -20,7 +20,7 @@ import {
KIRO_CONFIG,
assertValidAwsRegion,
CURSOR_CONFIG,
KIMI_CODING_CONFIG,
KIMI_CONFIG,
KILOCODE_CONFIG,
CLINE_CONFIG,
CLINEPASS_CONFIG,
@@ -1081,13 +1081,22 @@ const PROVIDERS = {
}),
},
"kimi-coding": {
config: KIMI_CODING_CONFIG,
// Kimi Code device flow (CLIProxyAPI internal/auth/kimi). Id is `kimi`;
// `kimi-coding` remains an alias key so old UI/API routes still resolve.
kimi: {
config: KIMI_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const { buildKimiHeaders } = await import("open-sse/config/appConstants.js");
const deviceId = crypto.randomUUID();
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
...buildKimiHeaders(deviceId),
};
const response = await fetch(config.deviceCodeUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
headers,
body: new URLSearchParams({ client_id: config.clientId }),
});
if (!response.ok) {
@@ -1095,21 +1104,30 @@ const PROVIDERS = {
throw new Error(`Device code request failed: ${error}`);
}
const data = await response.json();
const authorizeDeviceUrl = config.authorizeDeviceUrl || "https://www.kimi.com/code/authorize_device";
return {
device_code: data.device_code,
user_code: data.user_code,
verification_uri: data.verification_uri || "https://www.kimi.com/code/authorize_device",
verification_uri: data.verification_uri || authorizeDeviceUrl,
verification_uri_complete:
data.verification_uri_complete ||
`https://www.kimi.com/code/authorize_device?user_code=${data.user_code}`,
`${authorizeDeviceUrl}?user_code=${data.user_code}`,
expires_in: data.expires_in,
interval: data.interval || 5,
_kimiDeviceId: deviceId,
};
},
pollToken: async (config, deviceCode) => {
pollToken: async (config, deviceCode, _codeVerifier, extraData) => {
const { buildKimiHeaders } = await import("open-sse/config/appConstants.js");
const deviceId = extraData?._kimiDeviceId;
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
...buildKimiHeaders(deviceId),
};
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
headers,
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: config.clientId,
@@ -1119,19 +1137,26 @@ const PROVIDERS = {
let data;
try {
data = await response.json();
} catch (e) {
const text = await response.text();
data = { error: "invalid_response", error_description: text };
} catch {
data = { error: "invalid_response", error_description: "non-json token response" };
}
return { ok: response.ok, data };
// CLIProxyAPI: Kimi returns 200 for pending states with error field
if (data.error === "authorization_pending" || data.error === "slow_down") {
return { ok: true, data };
}
if (data.access_token && deviceId) data._kimiDeviceId = deviceId;
return { ok: response.ok || !!data.access_token || !!data.error, data };
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
providerSpecificData: {
authMethod: "device_code",
...(tokens._kimiDeviceId ? { deviceId: tokens._kimiDeviceId } : {}),
},
}),
},
kilocode: {
config: KILOCODE_CONFIG,
flowType: "device_code",
@@ -1520,7 +1545,9 @@ const PROVIDERS = {
* Get provider handler
*/
export function getProvider(name) {
const provider = PROVIDERS[name];
// Legacy kimi-coding → kimi (dual-auth merge)
const key = name === "kimi-coding" ? "kimi" : name;
const provider = PROVIDERS[key];
if (!provider) {
throw new Error(`Unknown provider: ${name}`);
}
+3
View File
@@ -161,6 +161,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
"github",
"qwen",
"kiro",
"kimi",
"kimi-coding",
"kilocode",
"codebuddy-cn",
@@ -206,6 +207,8 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
_qoderMachineId: data._qoderMachineId,
_qoderVerifier: data.codeVerifier,
}
: (provider === "kimi" || provider === "kimi-coding")
? { _kimiDeviceId: data._kimiDeviceId }
: null;
startPolling(
data.device_code,
+5 -3
View File
@@ -10,7 +10,7 @@
"kr": "kiro",
"cu": "cursor",
"kc": "kilocode",
"kmc": "kimi-coding",
"kmc": "kimi",
"cl": "cline",
"oc": "opencode",
"ocg": "opencode-go",
@@ -117,6 +117,7 @@
"cursor": "cu",
"deepgram": "deepgram",
"deepseek": "deepseek",
"featherless": "featherless",
"fireworks": "fireworks",
"gemini": "gemini",
"gemini-cli": "gc",
@@ -132,7 +133,6 @@
"kilocode": "kc",
"kimchi": "kimchi",
"kimi": "kimi",
"kimi-coding": "kmc",
"kiro": "kr",
"mimo-free": "mmf",
"minimax": "minimax",
@@ -149,6 +149,7 @@
"opencode-go": "opencode-go",
"openrouter": "openrouter",
"perplexity": "perplexity",
"perplexity-agent": "perplexity-agent",
"perplexity-web": "perplexity-web",
"qoder": "qd",
"qwen": "qw",
@@ -188,6 +189,7 @@
"edge-tts",
"elevenlabs-tts-models",
"fal-ai",
"featherless",
"fireworks",
"gc",
"gcli",
@@ -206,7 +208,6 @@
"kc",
"kimchi",
"kimi",
"kmc",
"kr",
"local-device",
"minimax",
@@ -226,6 +227,7 @@
"openrouter-tts-models",
"openrouter-tts-voices",
"perplexity",
"perplexity-agent",
"perplexity-web",
"qd",
"qw",
+3 -3
View File
@@ -35,14 +35,14 @@
"xai": "https://auth.x.ai/oauth2/token",
"grok-cli": "https://auth.x.ai/oauth2/token",
"cline": "https://api.cline.bot/api/v1/auth/token",
"kimi-coding": "https://auth.kimi.com/api/oauth/token"
"kimi": "https://auth.kimi.com/api/oauth/token"
},
"authUrls": {
"kiro": "https://prod.us-east-1.auth.desktop.kiro.dev"
},
"refreshUrls": {
"cline": "https://api.cline.bot/api/v1/auth/refresh",
"kimi-coding": "https://auth.kimi.com/api/oauth/token",
"kimi": "https://auth.kimi.com/api/oauth/token",
"xai": "https://auth.x.ai/oauth2/token",
"grok-cli": "https://auth.x.ai/oauth2/token"
},
@@ -51,7 +51,7 @@
"codex": "app_EMoamEEZ73f0CkXaXp7hrann",
"qwen": "f0304373b74a44d2b584a3fb70ca9e56",
"iflow": "10009311001",
"kimi-coding": "17e5f671-d194-4dfb-9706-5516cb48c098",
"kimi": "17e5f671-d194-4dfb-9706-5516cb48c098",
"grok-cli": "b1a00492-073a-47ea-816f-4c329264a828"
}
}
+25 -53
View File
@@ -1,6 +1,6 @@
{
"alicode-intl": {
"baseUrl": "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions",
"baseUrl": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"headers": {},
"quirks": {
"preserveCacheControl": true
@@ -19,18 +19,17 @@
"baseUrl": "https://api.anthropic.com/v1/messages",
"format": "claude",
"headers": {
"Anthropic-Version": "2023-06-01",
"anthropic-version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14"
}
},
"antigravity": {
"baseUrls": [
"https://daily-cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com"
"https://cloudcode-pa.googleapis.com"
],
"format": "antigravity",
"headers": {
"User-Agent": "antigravity/1.107.0 darwin/arm64"
"User-Agent": "antigravity/ide/2.1.1 darwin/arm64"
},
"retry": {
"429": {
@@ -270,6 +269,11 @@
}
]
},
"featherless": {
"baseUrl": "https://api.featherless.ai/v1/chat/completions",
"validateUrl": "https://api.featherless.ai/v1/models",
"format": "openai"
},
"fireworks": {
"baseUrl": "https://api.fireworks.ai/inference/v1/chat/completions",
"validateUrl": "https://api.fireworks.ai/inference/v1/models",
@@ -307,6 +311,7 @@
"github": {
"baseUrl": "https://api.githubcopilot.com/chat/completions",
"responsesUrl": "https://api.githubcopilot.com/responses",
"messagesUrl": "https://api.githubcopilot.com/v1/messages",
"headers": {
"copilot-integration-id": "vscode-chat",
"editor-version": "vscode/1.110.0",
@@ -398,17 +403,18 @@
"modelsUrl": "https://cli-chat-proxy.grok.com/v1/models",
"userUrl": "https://cli-chat-proxy.grok.com/v1/user",
"billingUrl": "https://cli-chat-proxy.grok.com/v1/billing",
"clientVersion": "0.2.93",
"clientIdentifier": "grok-pager",
"clientVersion": "0.2.99",
"clientIdentifier": "grok-shell",
"tokenAuth": "xai-grok-cli",
"headers": {
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-identifier": "grok-pager",
"x-grok-client-version": "0.2.93",
"x-authenticateresponse": "authenticate-response"
"User-Agent": "grok-shell/0.2.99 (linux; x86_64)",
"x-grok-client-identifier": "grok-shell",
"x-grok-client-version": "0.2.99"
},
"usage": {
"url": "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
"userUrl": "https://cli-chat-proxy.grok.com/v1/user?include=subscription"
},
"compactionAt": 400000,
"retry": {
"429": {
"attempts": 2,
@@ -477,7 +483,7 @@
"scheme": "bearer"
}
},
"kimi-coding": {
"kimi": {
"baseUrl": "https://api.kimi.com/coding/v1/messages",
"format": "claude",
"urlSuffix": "?beta=true",
@@ -528,45 +534,6 @@
}
]
},
"kimi": {
"baseUrl": "https://api.kimi.com/coding/v1/messages",
"format": "claude",
"urlSuffix": "?beta=true",
"headers": {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14"
},
"auth": {
"combined": true,
"header": "x-api-key",
"scheme": "raw"
},
"transports": [
{
"format": "openai",
"baseUrl": "https://api.kimi.com/coding/v1/chat/completions",
"auth": {
"combined": true,
"header": "Authorization",
"scheme": "bearer"
}
},
{
"format": "claude",
"baseUrl": "https://api.kimi.com/coding/v1/messages",
"urlSuffix": "?beta=true",
"headers": {
"Anthropic-Version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14"
},
"auth": {
"combined": true,
"header": "x-api-key",
"scheme": "raw"
}
}
]
},
"kiro": {
"baseUrl": "https://runtime.us-east-1.kiro.dev/generateAssistantResponse",
"baseUrls": [
@@ -774,6 +741,11 @@
"validateUrl": "https://api.perplexity.ai/models",
"format": "openai"
},
"perplexity-agent": {
"baseUrl": "https://api.perplexity.ai/v1/responses",
"validateUrl": "https://api.perplexity.ai/v1/models",
"format": "openai-responses"
},
"qoder": {
"baseUrl": "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation",
"headers": {},
+3 -3
View File
@@ -22,7 +22,7 @@ const resolved = {
// Grok CLI injects oauth.tokenUrl onto PROVIDERS via OAUTH_INJECT_FIELDS
"grok-cli": PROVIDERS["grok-cli"]?.tokenUrl,
cline: PROVIDERS.cline?.tokenUrl,
"kimi-coding": PROVIDERS["kimi-coding"]?.tokenUrl,
kimi: PROVIDERS.kimi?.tokenUrl,
},
authUrls: {
qwen: PROVIDERS.qwen?.authUrl,
@@ -31,7 +31,7 @@ const resolved = {
},
refreshUrls: {
cline: PROVIDERS.cline?.refreshUrl,
"kimi-coding": PROVIDERS["kimi-coding"]?.refreshUrl,
kimi: PROVIDERS.kimi?.refreshUrl,
xai: PROVIDERS.xai?.refreshUrl,
"grok-cli": PROVIDERS["grok-cli"]?.tokenUrl,
},
@@ -40,7 +40,7 @@ const resolved = {
codex: PROVIDERS.codex?.clientId,
qwen: PROVIDERS.qwen?.clientId,
iflow: PROVIDERS.iflow?.clientId,
"kimi-coding": PROVIDERS["kimi-coding"]?.clientId,
kimi: PROVIDERS.kimi?.clientId,
"grok-cli": PROVIDERS["grok-cli"]?.clientId,
},
};
+2 -1
View File
@@ -152,7 +152,8 @@ describe("Codex Refresh Token", () => {
expect(getRefreshLeadMs("claude")).toBe(4 * 60 * 60 * 1000); // 4 hours
expect(getRefreshLeadMs("iflow")).toBe(24 * 60 * 60 * 1000); // 24 hours
expect(getRefreshLeadMs("qwen")).toBe(20 * 60 * 1000); // 20 minutes
expect(getRefreshLeadMs("kimi-coding")).toBe(5 * 60 * 1000); // 5 minutes
expect(getRefreshLeadMs("kimi")).toBe(5 * 60 * 1000); // 5 minutes
expect(getRefreshLeadMs("kimi-coding")).toBe(5 * 60 * 1000); // legacy alias
expect(getRefreshLeadMs("antigravity")).toBe(5 * 60 * 1000); // 5 minutes
});