From 68566f53dc43b1249fad7382050fd7964186a46d Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 17 Jul 2026 12:09:14 +0700 Subject: [PATCH] feat(kimi): merge OAuth into dual-auth provider, add K3/K2.7 models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cli/src/cli/menus/providers.js | 2 +- open-sse/config/appConstants.js | 45 +++++++-- open-sse/executors/default.js | 18 ++-- open-sse/providers/capabilities.js | 12 ++- open-sse/providers/pricing.js | 9 ++ open-sse/providers/registry/index.js | 94 +++++++++---------- open-sse/providers/registry/kimi-coding.js | 65 ------------- open-sse/providers/registry/kimi.js | 47 ++++++++-- open-sse/services/tokenRefresh.js | 10 +- open-sse/services/tokenRefresh/providers.js | 48 +++++++++- .../dashboard/providers/[id]/page.js | 18 +++- .../api/oauth/[provider]/[action]/route.js | 11 ++- src/app/api/providers/[id]/test/testUtils.js | 3 +- src/lib/oauth/constants/oauth.js | 19 ++-- src/lib/oauth/providers.js | 55 ++++++++--- src/shared/components/OAuthModal.js | 3 + tests/__baseline__/alias-baseline.json | 8 +- tests/__baseline__/oauth-urls-baseline.json | 6 +- tests/__baseline__/providers-baseline.json | 78 +++++---------- tests/__baseline__/verify-oauth-urls.mjs | 6 +- tests/unit/codex-refresh-token.test.js | 3 +- 21 files changed, 332 insertions(+), 228 deletions(-) delete mode 100644 open-sse/providers/registry/kimi-coding.js diff --git a/cli/src/cli/menus/providers.js b/cli/src/cli/menus/providers.js index 57013466..291ae674 100644 --- a/cli/src/cli/menus/providers.js +++ b/cli/src/cli/menus/providers.js @@ -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" }, diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js index cc46697c..2934fc88 100644 --- a/open-sse/config/appConstants.js +++ b/open-sse/config/appConstants.js @@ -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, }; } diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 96a230b3..48be8350 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -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(); diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index 6e54a683..1d03d251 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -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 } }, diff --git a/open-sse/providers/pricing.js b/open-sse/providers/pricing.js index 943365a1..c2831fdb 100644 --- a/open-sse/providers/pricing.js +++ b/open-sse/providers/pricing.js @@ -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 } }, diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index e4cf5439..7b565b7d 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -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 ]; diff --git a/open-sse/providers/registry/kimi-coding.js b/open-sse/providers/registry/kimi-coding.js deleted file mode 100644 index 15705a86..00000000 --- a/open-sse/providers/registry/kimi-coding.js +++ /dev/null @@ -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, - }, -}; diff --git a/open-sse/providers/registry/kimi.js b/open-sse/providers/registry/kimi.js index e22286b4..88bfde3e 100644 --- a/open-sse/providers/registry/kimi.js +++ b/open-sse/providers/registry/kimi.js @@ -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, }, }; diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index c8d264c6..55f4582e 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -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 }; diff --git a/open-sse/services/tokenRefresh/providers.js b/open-sse/services/tokenRefresh/providers.js index 54c2fa0b..3fc8c4b2 100644 --- a/open-sse/services/tokenRefresh/providers.js +++ b/open-sse/services/tokenRefresh/providers.js @@ -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 () => { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index dff51fe0..310d7ff4 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -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() ? ( {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()} @@ -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" /> )} diff --git a/src/app/api/oauth/[provider]/[action]/route.js b/src/app/api/oauth/[provider]/[action]/route.js index 57bff272..eb4a7940 100644 --- a/src/app/api/oauth/[provider]/[action]/route.js +++ b/src/app/api/oauth/[provider]/[action]/route.js @@ -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 diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index a05e6f4e..8afdbf93 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -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`, diff --git a/src/lib/oauth/constants/oauth.js b/src/lib/oauth/constants/oauth.js index 1188b1fb..53015258 100644 --- a/src/lib/oauth/constants/oauth.js +++ b/src/lib/oauth/constants/oauth.js @@ -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", diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js index d2d14daf..e995567e 100644 --- a/src/lib/oauth/providers.js +++ b/src/lib/oauth/providers.js @@ -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}`); } diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js index 6d7b2bba..f734f6ab 100644 --- a/src/shared/components/OAuthModal.js +++ b/src/shared/components/OAuthModal.js @@ -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, diff --git a/tests/__baseline__/alias-baseline.json b/tests/__baseline__/alias-baseline.json index c104335e..866b75c7 100644 --- a/tests/__baseline__/alias-baseline.json +++ b/tests/__baseline__/alias-baseline.json @@ -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", diff --git a/tests/__baseline__/oauth-urls-baseline.json b/tests/__baseline__/oauth-urls-baseline.json index e2049194..b4468678 100644 --- a/tests/__baseline__/oauth-urls-baseline.json +++ b/tests/__baseline__/oauth-urls-baseline.json @@ -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" } } \ No newline at end of file diff --git a/tests/__baseline__/providers-baseline.json b/tests/__baseline__/providers-baseline.json index 8635e9e4..22f4e74f 100644 --- a/tests/__baseline__/providers-baseline.json +++ b/tests/__baseline__/providers-baseline.json @@ -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": {}, diff --git a/tests/__baseline__/verify-oauth-urls.mjs b/tests/__baseline__/verify-oauth-urls.mjs index b0c6a8fa..d385d102 100644 --- a/tests/__baseline__/verify-oauth-urls.mjs +++ b/tests/__baseline__/verify-oauth-urls.mjs @@ -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, }, }; diff --git a/tests/unit/codex-refresh-token.test.js b/tests/unit/codex-refresh-token.test.js index 3b702af8..e6274c16 100644 --- a/tests/unit/codex-refresh-token.test.js +++ b/tests/unit/codex-refresh-token.test.js @@ -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 });