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
parent bc252ea802
commit 68566f53dc
21 changed files with 332 additions and 228 deletions

View File

@@ -131,7 +131,7 @@ const APIKEY_PROVIDERS = {
openrouter: { id: "openrouter", name: "OpenRouter" }, openrouter: { id: "openrouter", name: "OpenRouter" },
glm: { id: "glm", name: "GLM Coding" }, glm: { id: "glm", name: "GLM Coding" },
minimax: { id: "minimax", name: "Minimax Coding" }, minimax: { id: "minimax", name: "Minimax Coding" },
kimi: { id: "kimi", name: "Kimi Coding" }, kimi: { id: "kimi", name: "Kimi" },
openai: { id: "openai", name: "OpenAI" }, openai: { id: "openai", name: "OpenAI" },
anthropic: { id: "anthropic", name: "Anthropic" }, anthropic: { id: "anthropic", name: "Anthropic" },
gemini: { id: "gemini", name: "Gemini" }, gemini: { id: "gemini", name: "Gemini" },

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 { PROVIDERS, PROVIDER_OAUTH } from "./providers.js";
import { ANTIGRAVITY_IDE_USER_AGENT } from "../providers/shared.js"; import { ANTIGRAVITY_IDE_USER_AGENT } from "../providers/shared.js";
import { createRequire } from "module";
// === Gemini CLI === derive từ registry gemini-cli.transport // === Gemini CLI === derive từ registry gemini-cli.transport
export const GEMINI_CLI_VERSION = PROVIDERS["gemini-cli"]?.cliVersion; 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 }, github: { token: PROVIDER_OAUTH["github"]?.tokenUrl, auth: PROVIDER_OAUTH["github"]?.authorizeUrl, deviceCode: PROVIDER_OAUTH["github"]?.deviceCodeUrl },
}; };
// Generate Kimi OAuth custom headers let _appVersion;
export function buildKimiHeaders() { 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 { return {
"X-Msh-Platform": "9router", "X-Msh-Platform": "9router",
"X-Msh-Version": "2.1.2", "X-Msh-Version": getAppPackageVersion(),
"X-Msh-Device-Model": typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown", "X-Msh-Device-Name": deviceName,
"X-Msh-Device-Id": `kimi-${Date.now()}` "X-Msh-Device-Model": deviceModel,
"X-Msh-Device-Id": resolvedId,
}; };
} }

View File

@@ -38,7 +38,8 @@ function applyAuth(headers, desc, credentials) {
// Provider-specific header quirks kept as small hooks (not pure auth). // Provider-specific header quirks kept as small hooks (not pure auth).
const HEADER_HOOKS = { 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)), 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; }, kilocodeOrg: (h, c) => { if (c.providerSpecificData?.orgId) h["X-Kilocode-OrganizationID"] = c.providerSpecificData.orgId; },
claudeOverlay: (h) => { claudeOverlay: (h) => {
@@ -227,7 +228,8 @@ export class DefaultExecutor extends BaseExecutor {
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions), kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions), cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
clinepass: () => 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) kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions)
}; };
@@ -307,16 +309,20 @@ export class DefaultExecutor extends BaseExecutor {
return { accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn }; return { accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
} }
async refreshKimiCoding(refreshToken, proxyOptions = null) { // CLIProxyAPI DeviceFlowClient.RefreshToken — form body + X-Msh-* headers + stable device_id
const kimiHeaders = buildKimiHeaders(); async refreshKimi(credentials, proxyOptions = null) {
const response = await proxyAwareFetch(PROVIDERS["kimi-coding"].refreshUrl, { 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", method: "POST",
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json", "Accept": "application/json",
...kimiHeaders ...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); }, proxyOptions);
if (!response.ok) return null; if (!response.ok) return null;
const tokens = await response.json(); const tokens = await response.json();

View File

@@ -96,6 +96,14 @@ export const MODEL_CAPABILITIES = {
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases // Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
"vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 }, "vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
"coder-model": { 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 }; 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 } }, { pattern: "*qwen*", caps: { reasoning: true, thinkingFormat: "qwen", contextWindow: 262144 } },
// ── Kimi (enabled→reasoning_effort; K2.7-code cannot disable) ───── // ── 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*k2*", caps: { vision: true, reasoning: true, thinkingFormat: "kimi", contextWindow: 262144, maxOutput: 262144 } },
{ pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } }, { pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },

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 }, "qwen3-coder-flash": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
// === Kimi === // === 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": { 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-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": { 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.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 }, "kimi-latest": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
// === DeepSeek === // === DeepSeek ===
@@ -185,6 +193,7 @@ export const PATTERN_PRICING = [
// --- Kimi --- // --- Kimi ---
{ pattern: "kimi-*-thinking", pricing: { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 } }, { 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-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 } }, { pattern: "kimi-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } },

View File

@@ -52,53 +52,52 @@ import p49 from "./jina-ai.js";
import p50 from "./jina-reader.js"; import p50 from "./jina-reader.js";
import p51 from "./kilocode.js"; import p51 from "./kilocode.js";
import p52 from "./kimchi.js"; import p52 from "./kimchi.js";
import p53 from "./kimi-coding.js"; import p53 from "./kimi.js";
import p54 from "./kimi.js"; import p54 from "./kiro.js";
import p55 from "./kiro.js"; import p55 from "./linkup.js";
import p56 from "./linkup.js"; import p56 from "./local-device.js";
import p57 from "./local-device.js"; import p57 from "./mimo-free.js";
import p58 from "./mimo-free.js"; import p58 from "./minimax-cn.js";
import p59 from "./minimax-cn.js"; import p59 from "./minimax.js";
import p60 from "./minimax.js"; import p60 from "./mistral.js";
import p61 from "./mistral.js"; import p61 from "./mmf.js";
import p62 from "./mmf.js"; import p62 from "./nanobanana.js";
import p63 from "./nanobanana.js"; import p63 from "./nebius.js";
import p64 from "./nebius.js"; import p64 from "./nvidia.js";
import p65 from "./nvidia.js"; import p65 from "./ollama-local.js";
import p66 from "./ollama-local.js"; import p66 from "./ollama.js";
import p67 from "./ollama.js"; import p67 from "./openai.js";
import p68 from "./openai.js"; import p68 from "./opencode-go.js";
import p69 from "./opencode-go.js"; import p69 from "./opencode.js";
import p70 from "./opencode.js"; import p70 from "./openrouter.js";
import p71 from "./openrouter.js"; import p71 from "./perplexity-web.js";
import p72 from "./perplexity-web.js"; import p72 from "./perplexity.js";
import p73 from "./perplexity.js"; import p73 from "./perplexity-agent.js";
import p74 from "./perplexity-agent.js"; import p74 from "./playht.js";
import p75 from "./playht.js"; import p75 from "./qoder.js";
import p76 from "./qoder.js"; import p76 from "./qwen.js";
import p77 from "./qwen.js"; import p77 from "./recraft.js";
import p78 from "./recraft.js"; import p78 from "./runwayml.js";
import p79 from "./runwayml.js"; import p79 from "./sdwebui.js";
import p80 from "./sdwebui.js"; import p80 from "./searchapi.js";
import p81 from "./searchapi.js"; import p81 from "./searxng.js";
import p82 from "./searxng.js"; import p82 from "./serper.js";
import p83 from "./serper.js"; import p83 from "./siliconflow.js";
import p84 from "./siliconflow.js"; import p84 from "./stability-ai.js";
import p85 from "./stability-ai.js"; import p85 from "./tavily.js";
import p86 from "./tavily.js"; import p86 from "./together.js";
import p87 from "./together.js"; import p87 from "./topaz.js";
import p88 from "./topaz.js"; import p88 from "./tortoise.js";
import p89 from "./tortoise.js"; import p89 from "./venice.js";
import p90 from "./venice.js"; import p90 from "./vercel-ai-gateway.js";
import p91 from "./vercel-ai-gateway.js"; import p91 from "./vertex-partner.js";
import p92 from "./vertex-partner.js"; import p92 from "./vertex.js";
import p93 from "./vertex.js"; import p93 from "./volcengine-ark.js";
import p94 from "./volcengine-ark.js"; import p94 from "./voyage-ai.js";
import p95 from "./voyage-ai.js"; import p95 from "./xai.js";
import p96 from "./xai.js"; import p96 from "./xiaomi-mimo.js";
import p97 from "./xiaomi-mimo.js"; import p97 from "./xiaomi-tokenplan.js";
import p98 from "./xiaomi-tokenplan.js"; import p98 from "./youcom.js";
import p99 from "./youcom.js";
export default [ export default [
p0, p0,
@@ -200,5 +199,4 @@ export default [
p96, p96,
p97, p97,
p98, p98,
p99
]; ];

View File

@@ -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,
},
};

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 { export default {
id: "kimi", id: "kimi",
priority: 170, priority: 170,
alias: "kimi", alias: "kimi",
// Legacy id + short alias from former kimi-coding registry entry
aliases: ["kimi-coding", "kmc"],
display: { display: {
name: "Kimi", name: "Kimi",
icon: "psychology", icon: "psychology",
@@ -12,18 +17,25 @@ export default {
website: "https://kimi.moonshot.cn", website: "https://kimi.moonshot.cn",
notice: { notice: {
apiKeyUrl: "https://platform.moonshot.ai/console/api-keys", apiKeyUrl: "https://platform.moonshot.ai/console/api-keys",
signupUrl: "https://www.kimi.com/code",
}, },
}, },
category: "apikey", category: "oauth",
authModes: ["oauth", "apikey"],
hasOAuth: true,
transport: { transport: {
baseUrl: "https://api.kimi.com/coding/v1/messages", baseUrl: "https://api.kimi.com/coding/v1/messages",
format: "claude", format: "claude",
urlSuffix: "?beta=true", urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS }, 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: { auth: {
combined: true, combined: true,
header: "x-api-key", header: "x-api-key",
scheme: "raw", scheme: "raw",
hooks: ["kimiHeaders"],
}, },
}, },
// Multi-endpoint: pick the transport matching client sourceFormat to skip translation. // Multi-endpoint: pick the transport matching client sourceFormat to skip translation.
@@ -31,26 +43,47 @@ export default {
{ {
format: "openai", format: "openai",
baseUrl: "https://api.kimi.com/coding/v1/chat/completions", 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", format: "claude",
baseUrl: "https://api.kimi.com/coding/v1/messages", baseUrl: "https://api.kimi.com/coding/v1/messages",
urlSuffix: "?beta=true", urlSuffix: "?beta=true",
headers: { ...CLAUDE_API_HEADERS }, 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: [ 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.6", name: "Kimi K2.6" },
{ id: "kimi-k2.5", name: "Kimi K2.5" }, { id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" }, { id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" }, { id: "kimi-latest", name: "Kimi Latest" },
], ],
serviceKinds: ["llm","webSearch"], serviceKinds: ["llm", "webSearch"],
searchViaChat: { searchViaChat: {
defaultModel: "kimi-k2.5", defaultModel: "kimi-k3",
endpoint: "https://api.moonshot.cn/v1/chat/completions", 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,
}, },
}; };

View File

@@ -3,6 +3,7 @@ import { OAUTH_ENDPOINTS, REFRESH_LEAD_MS } from "../config/appConstants.js";
import { import {
refreshXaiToken, refreshXaiToken,
refreshAccessToken, refreshAccessToken,
refreshKimiToken,
refreshClaudeOAuthToken, refreshClaudeOAuthToken,
refreshGoogleToken, refreshGoogleToken,
refreshQwenToken, refreshQwenToken,
@@ -18,6 +19,7 @@ import {
// Re-export all provider refresh functions (preserves public API for all consumers) // Re-export all provider refresh functions (preserves public API for all consumers)
export { export {
refreshAccessToken, refreshAccessToken,
refreshKimiToken,
refreshClaudeOAuthToken, refreshClaudeOAuthToken,
refreshGoogleToken, refreshGoogleToken,
refreshQwenToken, refreshQwenToken,
@@ -44,7 +46,10 @@ export function isUnrecoverableRefreshError(result) {
} }
export function getRefreshLeadMs(provider) { 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) { export function parseVertexSaJson(apiKey) {
@@ -133,6 +138,9 @@ const REFRESH_HANDLERS = {
"grok-cli": (c, log) => refreshXaiToken(c.refreshToken, log), "grok-cli": (c, log) => refreshXaiToken(c.refreshToken, log),
gcli: (c, log) => refreshXaiToken(c.refreshToken, log), gcli: (c, log) => refreshXaiToken(c.refreshToken, log),
"codebuddy-cn": (c, log) => refreshCodebuddyToken(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: vertexRefreshHandler,
"vertex-partner": vertexRefreshHandler "vertex-partner": vertexRefreshHandler
}; };

View File

@@ -1,5 +1,5 @@
import { PROVIDERS, PROVIDER_OAUTH } from "../../config/providers.js"; 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 { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { dedupRefresh } from "./dedup.js"; import { dedupRefresh } from "./dedup.js";
import { buildExternalIdpRefreshParams } from "../../../src/lib/oauth/kiroExternalIdp.js"; import { buildExternalIdpRefreshParams } from "../../../src/lib/oauth/kiroExternalIdp.js";
@@ -91,6 +91,52 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
}, log); }, 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) { export async function refreshClaudeOAuthToken(refreshToken, log) {
if (!refreshToken) return null; if (!refreshToken) return null;
return dedupRefresh("claude", refreshToken, async () => { return dedupRefresh("claude", refreshToken, async () => {

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; 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 { 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 { 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"; import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
@@ -151,8 +152,12 @@ export default function ProviderDetailPage() {
const oauthConnectionLabel = const oauthConnectionLabel =
providerId === "xai" ? "Grok Build OAuth" providerId === "xai" ? "Grok Build OAuth"
: providerId === "grok-cli" ? "Grok CLI Device Login" : providerId === "grok-cli" ? "Grok CLI Device Login"
: providerId === "kimi" ? "Kimi Coding OAuth"
: "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. // Resolve suffix "(level)" for a model when a thinking level is picked and the model supports it.
const resolveThinkingSuffix = (modelId) => { const resolveThinkingSuffix = (modelId) => {
if (!thinkingMode || thinkingMode === "auto") return null; if (!thinkingMode || thinkingMode === "auto") return null;
@@ -1231,7 +1236,7 @@ export default function ProviderDetailPage() {
if (isAnthropicCompatible) { if (isAnthropicCompatible) {
return "/providers/anthropic-m.png"; return "/providers/anthropic-m.png";
} }
return `/providers/${providerInfo.id}.png`; return getProviderIconSrc(providerInfo.id);
}; };
return ( return (
@@ -1250,7 +1255,7 @@ export default function ProviderDetailPage() {
className="flex size-12 shrink-0 items-center justify-center rounded-lg" className="flex size-12 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: `${providerInfo.color}15` }} style={{ backgroundColor: `${providerInfo.color}15` }}
> >
{headerImgError ? ( {headerImgError || !getHeaderIconPath() ? (
<span className="text-sm font-bold" style={{ color: providerInfo.color }}> <span className="text-sm font-bold" style={{ color: providerInfo.color }}>
{providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()} {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()}
</span> </span>
@@ -1262,7 +1267,12 @@ export default function ProviderDetailPage() {
height={48} height={48}
className="max-h-12 max-w-12 rounded-lg object-contain" className="max-h-12 max-w-12 rounded-lg object-contain"
sizes="48px" sizes="48px"
onError={() => setHeaderImgError(true)} onError={() => {
markProviderIconMissing(providerInfo.id);
setHeaderImgError(true);
}}
loading="lazy"
decoding="async"
/> />
)} )}
</div> </div>

View File

@@ -154,6 +154,7 @@ export async function GET(request, { params }) {
const noPkceDeviceProviders = [ const noPkceDeviceProviders = [
"github", "github",
"kiro", "kiro",
"kimi",
"kimi-coding", "kimi-coding",
"kilocode", "kilocode",
"codebuddy-cn", "codebuddy-cn",
@@ -279,10 +280,11 @@ export async function POST(request, { params }) {
} }
// Providers that don't use PKCE for device code // 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; let result;
if (noPkceProviders.includes(provider)) { 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") { } else if (provider === "kiro") {
// Kiro needs extraData (clientId, clientSecret) from device code response // Kiro needs extraData (clientId, clientSecret) from device code response
result = await pollForToken(provider, deviceCode, null, extraData); result = await pollForToken(provider, deviceCode, null, extraData);
@@ -303,9 +305,10 @@ export async function POST(request, { params }) {
} }
if (result.success) { 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({ const connection = await createProviderConnection({
provider, provider: providerId,
authType: "oauth", authType: "oauth",
...result.tokens, ...result.tokens,
expiresAt: result.tokens.expiresIn expiresAt: result.tokens.expiresIn

View File

@@ -75,7 +75,8 @@ const OAUTH_TEST_CONFIG = {
authPrefix: "Bearer ", authPrefix: "Bearer ",
refreshable: false, refreshable: false,
}, },
"kimi-coding": { checkExpiry: true, refreshable: false }, kimi: { checkExpiry: true, refreshable: true },
"kimi-coding": { checkExpiry: true, refreshable: true },
cursor: { tokenExists: true }, cursor: { tokenExists: true },
kilocode: { kilocode: {
url: `${KILOCODE_CONFIG.apiBaseUrl}/api/profile`, url: `${KILOCODE_CONFIG.apiBaseUrl}/api/profile`,

View File

@@ -89,12 +89,18 @@ export const CURSOR_CONFIG = {
}, },
}; };
// Kimi Coding OAuth Configuration (Device Code Flow) // Kimi Code OAuth (Device Code Flow) — merged into provider id `kimi` (dual auth)
// clientId uses env override — dynamic, not stored in registry // clientId: registry first, env override for forks
export const KIMI_CODING_CONFIG = { export const KIMI_CONFIG = {
...PROVIDER_OAUTH["kimi-coding"], ...PROVIDER_OAUTH["kimi"],
clientId: process.env.KIMI_CODING_OAUTH_CLIENT_ID || REGISTRY_PROVIDERS["kimi-coding"]?.clientId, 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) // KiloCode OAuth Configuration (Custom Device Auth Flow)
export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] }; export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] };
@@ -134,7 +140,8 @@ export const PROVIDERS = {
GITHUB: "github", GITHUB: "github",
KIRO: "kiro", KIRO: "kiro",
CURSOR: "cursor", CURSOR: "cursor",
KIMI_CODING: "kimi-coding", KIMI: "kimi",
KIMI_CODING: "kimi",
KILOCODE: "kilocode", KILOCODE: "kilocode",
CLINE: "cline", CLINE: "cline",
CLINEPASS: "clinepass", CLINEPASS: "clinepass",

View File

@@ -20,7 +20,7 @@ import {
KIRO_CONFIG, KIRO_CONFIG,
assertValidAwsRegion, assertValidAwsRegion,
CURSOR_CONFIG, CURSOR_CONFIG,
KIMI_CODING_CONFIG, KIMI_CONFIG,
KILOCODE_CONFIG, KILOCODE_CONFIG,
CLINE_CONFIG, CLINE_CONFIG,
CLINEPASS_CONFIG, CLINEPASS_CONFIG,
@@ -1081,13 +1081,22 @@ const PROVIDERS = {
}), }),
}, },
"kimi-coding": { // Kimi Code device flow (CLIProxyAPI internal/auth/kimi). Id is `kimi`;
config: KIMI_CODING_CONFIG, // `kimi-coding` remains an alias key so old UI/API routes still resolve.
kimi: {
config: KIMI_CONFIG,
flowType: "device_code", flowType: "device_code",
requestDeviceCode: async (config) => { 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, { const response = await fetch(config.deviceCodeUrl, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, headers,
body: new URLSearchParams({ client_id: config.clientId }), body: new URLSearchParams({ client_id: config.clientId }),
}); });
if (!response.ok) { if (!response.ok) {
@@ -1095,21 +1104,30 @@ const PROVIDERS = {
throw new Error(`Device code request failed: ${error}`); throw new Error(`Device code request failed: ${error}`);
} }
const data = await response.json(); const data = await response.json();
const authorizeDeviceUrl = config.authorizeDeviceUrl || "https://www.kimi.com/code/authorize_device";
return { return {
device_code: data.device_code, device_code: data.device_code,
user_code: data.user_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: verification_uri_complete:
data.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, expires_in: data.expires_in,
interval: data.interval || 5, 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, { const response = await fetch(config.tokenUrl, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, headers,
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code", grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: config.clientId, client_id: config.clientId,
@@ -1119,19 +1137,26 @@ const PROVIDERS = {
let data; let data;
try { try {
data = await response.json(); data = await response.json();
} catch (e) { } catch {
const text = await response.text(); data = { error: "invalid_response", error_description: "non-json token response" };
data = { error: "invalid_response", error_description: text };
} }
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) => ({ mapTokens: (tokens) => ({
accessToken: tokens.access_token, accessToken: tokens.access_token,
refreshToken: tokens.refresh_token, refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in, expiresIn: tokens.expires_in,
providerSpecificData: {
authMethod: "device_code",
...(tokens._kimiDeviceId ? { deviceId: tokens._kimiDeviceId } : {}),
},
}), }),
}, },
kilocode: { kilocode: {
config: KILOCODE_CONFIG, config: KILOCODE_CONFIG,
flowType: "device_code", flowType: "device_code",
@@ -1520,7 +1545,9 @@ const PROVIDERS = {
* Get provider handler * Get provider handler
*/ */
export function getProvider(name) { 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) { if (!provider) {
throw new Error(`Unknown provider: ${name}`); throw new Error(`Unknown provider: ${name}`);
} }

View File

@@ -161,6 +161,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
"github", "github",
"qwen", "qwen",
"kiro", "kiro",
"kimi",
"kimi-coding", "kimi-coding",
"kilocode", "kilocode",
"codebuddy-cn", "codebuddy-cn",
@@ -206,6 +207,8 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
_qoderMachineId: data._qoderMachineId, _qoderMachineId: data._qoderMachineId,
_qoderVerifier: data.codeVerifier, _qoderVerifier: data.codeVerifier,
} }
: (provider === "kimi" || provider === "kimi-coding")
? { _kimiDeviceId: data._kimiDeviceId }
: null; : null;
startPolling( startPolling(
data.device_code, data.device_code,

View File

@@ -10,7 +10,7 @@
"kr": "kiro", "kr": "kiro",
"cu": "cursor", "cu": "cursor",
"kc": "kilocode", "kc": "kilocode",
"kmc": "kimi-coding", "kmc": "kimi",
"cl": "cline", "cl": "cline",
"oc": "opencode", "oc": "opencode",
"ocg": "opencode-go", "ocg": "opencode-go",
@@ -117,6 +117,7 @@
"cursor": "cu", "cursor": "cu",
"deepgram": "deepgram", "deepgram": "deepgram",
"deepseek": "deepseek", "deepseek": "deepseek",
"featherless": "featherless",
"fireworks": "fireworks", "fireworks": "fireworks",
"gemini": "gemini", "gemini": "gemini",
"gemini-cli": "gc", "gemini-cli": "gc",
@@ -132,7 +133,6 @@
"kilocode": "kc", "kilocode": "kc",
"kimchi": "kimchi", "kimchi": "kimchi",
"kimi": "kimi", "kimi": "kimi",
"kimi-coding": "kmc",
"kiro": "kr", "kiro": "kr",
"mimo-free": "mmf", "mimo-free": "mmf",
"minimax": "minimax", "minimax": "minimax",
@@ -149,6 +149,7 @@
"opencode-go": "opencode-go", "opencode-go": "opencode-go",
"openrouter": "openrouter", "openrouter": "openrouter",
"perplexity": "perplexity", "perplexity": "perplexity",
"perplexity-agent": "perplexity-agent",
"perplexity-web": "perplexity-web", "perplexity-web": "perplexity-web",
"qoder": "qd", "qoder": "qd",
"qwen": "qw", "qwen": "qw",
@@ -188,6 +189,7 @@
"edge-tts", "edge-tts",
"elevenlabs-tts-models", "elevenlabs-tts-models",
"fal-ai", "fal-ai",
"featherless",
"fireworks", "fireworks",
"gc", "gc",
"gcli", "gcli",
@@ -206,7 +208,6 @@
"kc", "kc",
"kimchi", "kimchi",
"kimi", "kimi",
"kmc",
"kr", "kr",
"local-device", "local-device",
"minimax", "minimax",
@@ -226,6 +227,7 @@
"openrouter-tts-models", "openrouter-tts-models",
"openrouter-tts-voices", "openrouter-tts-voices",
"perplexity", "perplexity",
"perplexity-agent",
"perplexity-web", "perplexity-web",
"qd", "qd",
"qw", "qw",

View File

@@ -35,14 +35,14 @@
"xai": "https://auth.x.ai/oauth2/token", "xai": "https://auth.x.ai/oauth2/token",
"grok-cli": "https://auth.x.ai/oauth2/token", "grok-cli": "https://auth.x.ai/oauth2/token",
"cline": "https://api.cline.bot/api/v1/auth/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": { "authUrls": {
"kiro": "https://prod.us-east-1.auth.desktop.kiro.dev" "kiro": "https://prod.us-east-1.auth.desktop.kiro.dev"
}, },
"refreshUrls": { "refreshUrls": {
"cline": "https://api.cline.bot/api/v1/auth/refresh", "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", "xai": "https://auth.x.ai/oauth2/token",
"grok-cli": "https://auth.x.ai/oauth2/token" "grok-cli": "https://auth.x.ai/oauth2/token"
}, },
@@ -51,7 +51,7 @@
"codex": "app_EMoamEEZ73f0CkXaXp7hrann", "codex": "app_EMoamEEZ73f0CkXaXp7hrann",
"qwen": "f0304373b74a44d2b584a3fb70ca9e56", "qwen": "f0304373b74a44d2b584a3fb70ca9e56",
"iflow": "10009311001", "iflow": "10009311001",
"kimi-coding": "17e5f671-d194-4dfb-9706-5516cb48c098", "kimi": "17e5f671-d194-4dfb-9706-5516cb48c098",
"grok-cli": "b1a00492-073a-47ea-816f-4c329264a828" "grok-cli": "b1a00492-073a-47ea-816f-4c329264a828"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"alicode-intl": { "alicode-intl": {
"baseUrl": "https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions", "baseUrl": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"headers": {}, "headers": {},
"quirks": { "quirks": {
"preserveCacheControl": true "preserveCacheControl": true
@@ -19,18 +19,17 @@
"baseUrl": "https://api.anthropic.com/v1/messages", "baseUrl": "https://api.anthropic.com/v1/messages",
"format": "claude", "format": "claude",
"headers": { "headers": {
"Anthropic-Version": "2023-06-01", "anthropic-version": "2023-06-01",
"Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14" "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14"
} }
}, },
"antigravity": { "antigravity": {
"baseUrls": [ "baseUrls": [
"https://daily-cloudcode-pa.googleapis.com", "https://cloudcode-pa.googleapis.com"
"https://daily-cloudcode-pa.sandbox.googleapis.com"
], ],
"format": "antigravity", "format": "antigravity",
"headers": { "headers": {
"User-Agent": "antigravity/1.107.0 darwin/arm64" "User-Agent": "antigravity/ide/2.1.1 darwin/arm64"
}, },
"retry": { "retry": {
"429": { "429": {
@@ -270,6 +269,11 @@
} }
] ]
}, },
"featherless": {
"baseUrl": "https://api.featherless.ai/v1/chat/completions",
"validateUrl": "https://api.featherless.ai/v1/models",
"format": "openai"
},
"fireworks": { "fireworks": {
"baseUrl": "https://api.fireworks.ai/inference/v1/chat/completions", "baseUrl": "https://api.fireworks.ai/inference/v1/chat/completions",
"validateUrl": "https://api.fireworks.ai/inference/v1/models", "validateUrl": "https://api.fireworks.ai/inference/v1/models",
@@ -307,6 +311,7 @@
"github": { "github": {
"baseUrl": "https://api.githubcopilot.com/chat/completions", "baseUrl": "https://api.githubcopilot.com/chat/completions",
"responsesUrl": "https://api.githubcopilot.com/responses", "responsesUrl": "https://api.githubcopilot.com/responses",
"messagesUrl": "https://api.githubcopilot.com/v1/messages",
"headers": { "headers": {
"copilot-integration-id": "vscode-chat", "copilot-integration-id": "vscode-chat",
"editor-version": "vscode/1.110.0", "editor-version": "vscode/1.110.0",
@@ -398,17 +403,18 @@
"modelsUrl": "https://cli-chat-proxy.grok.com/v1/models", "modelsUrl": "https://cli-chat-proxy.grok.com/v1/models",
"userUrl": "https://cli-chat-proxy.grok.com/v1/user", "userUrl": "https://cli-chat-proxy.grok.com/v1/user",
"billingUrl": "https://cli-chat-proxy.grok.com/v1/billing", "billingUrl": "https://cli-chat-proxy.grok.com/v1/billing",
"clientVersion": "0.2.93", "clientVersion": "0.2.99",
"clientIdentifier": "grok-pager", "clientIdentifier": "grok-shell",
"tokenAuth": "xai-grok-cli", "tokenAuth": "xai-grok-cli",
"headers": { "headers": {
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)", "User-Agent": "grok-shell/0.2.99 (linux; x86_64)",
"x-xai-token-auth": "xai-grok-cli", "x-grok-client-identifier": "grok-shell",
"x-grok-client-identifier": "grok-pager", "x-grok-client-version": "0.2.99"
"x-grok-client-version": "0.2.93", },
"x-authenticateresponse": "authenticate-response" "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": { "retry": {
"429": { "429": {
"attempts": 2, "attempts": 2,
@@ -477,7 +483,7 @@
"scheme": "bearer" "scheme": "bearer"
} }
}, },
"kimi-coding": { "kimi": {
"baseUrl": "https://api.kimi.com/coding/v1/messages", "baseUrl": "https://api.kimi.com/coding/v1/messages",
"format": "claude", "format": "claude",
"urlSuffix": "?beta=true", "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": { "kiro": {
"baseUrl": "https://runtime.us-east-1.kiro.dev/generateAssistantResponse", "baseUrl": "https://runtime.us-east-1.kiro.dev/generateAssistantResponse",
"baseUrls": [ "baseUrls": [
@@ -774,6 +741,11 @@
"validateUrl": "https://api.perplexity.ai/models", "validateUrl": "https://api.perplexity.ai/models",
"format": "openai" "format": "openai"
}, },
"perplexity-agent": {
"baseUrl": "https://api.perplexity.ai/v1/responses",
"validateUrl": "https://api.perplexity.ai/v1/models",
"format": "openai-responses"
},
"qoder": { "qoder": {
"baseUrl": "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation", "baseUrl": "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation",
"headers": {}, "headers": {},

View File

@@ -22,7 +22,7 @@ const resolved = {
// Grok CLI injects oauth.tokenUrl onto PROVIDERS via OAUTH_INJECT_FIELDS // Grok CLI injects oauth.tokenUrl onto PROVIDERS via OAUTH_INJECT_FIELDS
"grok-cli": PROVIDERS["grok-cli"]?.tokenUrl, "grok-cli": PROVIDERS["grok-cli"]?.tokenUrl,
cline: PROVIDERS.cline?.tokenUrl, cline: PROVIDERS.cline?.tokenUrl,
"kimi-coding": PROVIDERS["kimi-coding"]?.tokenUrl, kimi: PROVIDERS.kimi?.tokenUrl,
}, },
authUrls: { authUrls: {
qwen: PROVIDERS.qwen?.authUrl, qwen: PROVIDERS.qwen?.authUrl,
@@ -31,7 +31,7 @@ const resolved = {
}, },
refreshUrls: { refreshUrls: {
cline: PROVIDERS.cline?.refreshUrl, cline: PROVIDERS.cline?.refreshUrl,
"kimi-coding": PROVIDERS["kimi-coding"]?.refreshUrl, kimi: PROVIDERS.kimi?.refreshUrl,
xai: PROVIDERS.xai?.refreshUrl, xai: PROVIDERS.xai?.refreshUrl,
"grok-cli": PROVIDERS["grok-cli"]?.tokenUrl, "grok-cli": PROVIDERS["grok-cli"]?.tokenUrl,
}, },
@@ -40,7 +40,7 @@ const resolved = {
codex: PROVIDERS.codex?.clientId, codex: PROVIDERS.codex?.clientId,
qwen: PROVIDERS.qwen?.clientId, qwen: PROVIDERS.qwen?.clientId,
iflow: PROVIDERS.iflow?.clientId, iflow: PROVIDERS.iflow?.clientId,
"kimi-coding": PROVIDERS["kimi-coding"]?.clientId, kimi: PROVIDERS.kimi?.clientId,
"grok-cli": PROVIDERS["grok-cli"]?.clientId, "grok-cli": PROVIDERS["grok-cli"]?.clientId,
}, },
}; };

View File

@@ -152,7 +152,8 @@ describe("Codex Refresh Token", () => {
expect(getRefreshLeadMs("claude")).toBe(4 * 60 * 60 * 1000); // 4 hours expect(getRefreshLeadMs("claude")).toBe(4 * 60 * 60 * 1000); // 4 hours
expect(getRefreshLeadMs("iflow")).toBe(24 * 60 * 60 * 1000); // 24 hours expect(getRefreshLeadMs("iflow")).toBe(24 * 60 * 60 * 1000); // 24 hours
expect(getRefreshLeadMs("qwen")).toBe(20 * 60 * 1000); // 20 minutes 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 expect(getRefreshLeadMs("antigravity")).toBe(5 * 60 * 1000); // 5 minutes
}); });