Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1afcc1b
feat: api key token control + qol
AntiTamper May 15, 2026
69c2e96
feat: show provider quota bars
AntiTamper May 15, 2026
00c2f45
fix: show remaining api key quota
AntiTamper May 15, 2026
714c252
fix: avoid runtime init during build
AntiTamper May 15, 2026
db42759
wip: provider quota and kimi follow-up
AntiTamper May 16, 2026
832790b
merge upstream master and fix kimi api support
AntiTamper May 16, 2026
ce911a6
fix kimi provider split and quota density
AntiTamper May 16, 2026
7e55b9e
fix: normalize claude tool choice
AntiTamper May 17, 2026
2e9c957
Merge remote-tracking branch 'origin/master' into work/provider-quota…
AntiTamper May 17, 2026
2217c0c
fix: harden provider url fetches
AntiTamper May 17, 2026
efe55b4
fix: support kimi code via claude clients
AntiTamper May 17, 2026
594d478
fix: suppress stale tunnel health pings
AntiTamper May 17, 2026
126a7ed
fix: improve token saver and kimi compatibility
AntiTamper May 17, 2026
efc3cd7
fix: refine caveman controls and standalone assets
AntiTamper May 17, 2026
1a6b7d9
security: harden dashboardGuard, fix memory leaks, mobile UI pass
AntiTamper May 18, 2026
ccb4be2
feat: integrate TOON lossless tool result compression
AntiTamper May 18, 2026
7f322d4
Merge remote-tracking branch 'origin/master' into feature/api-key-tok…
AntiTamper May 18, 2026
70c6b1d
fix(kimi/codex): normalize reasoning/thinking fields across streaming…
AntiTamper Jun 23, 2026
522eace
fix(content): capture content from output_text.delta and delta.messag…
AntiTamper Jun 23, 2026
c44a67d
fix(caveman/stream): custom system instructions for Responses API + K…
AntiTamper Jun 25, 2026
48522df
fix(cli/packaging): include src/ directory in npm package via .npmignore
AntiTamper Jun 25, 2026
a033cc3
fix(compact): wire _compact flag to toonEnabled so /v1/responses/comp…
AntiTamper Jun 25, 2026
72850e6
fix(deps): add @toon-format/toon to package.json dependencies
AntiTamper Jun 25, 2026
68ba671
fix(streaming): normalize delta format across transformer/converter p…
AntiTamper Jun 25, 2026
cdb6ea5
chore: remove dead files + add .next-prod to .gitignore
AntiTamper Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ next-env.d.ts
.bin/*
data/
logs/*
output/
.playwright-cli/
source/*
.cursor/*
docs/*
Expand All @@ -72,3 +74,5 @@ ecosystem.config.*
scripts/agSniffer/*
gitbooks/*
gitbook/README.md

/.next-prod/
4 changes: 2 additions & 2 deletions cli/.npmignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Ignore everything except what's in package.json "files"
*
!cli.js
!src/
!hooks/
!app/
!package.json
!README.md
!LICENSE

!LICENSE
9 changes: 6 additions & 3 deletions cli/src/cli/menus/cliTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async function buildClaudeHeader() {

const settings = result.data.settings;
const currentUrl = settings?.env?.ANTHROPIC_BASE_URL;
const currentKey = settings?.env?.ANTHROPIC_AUTH_TOKEN;
const currentKey = settings?.env?.ANTHROPIC_AUTH_TOKEN || settings?.env?.ANTHROPIC_API_KEY;
const lines = [];

if (currentUrl) {
Expand Down Expand Up @@ -85,7 +85,7 @@ async function claudeQuickSetup(port) {
return;
}

const env = { ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: apiKey, API_TIMEOUT_MS: "600000" };
const env = { ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: apiKey, ANTHROPIC_API_KEY: apiKey, API_TIMEOUT_MS: "600000" };
CLAUDE_MODEL_TYPES.forEach(t => { env[t.envKey] = t.defaultValue; });

const result = await api.applyCliToolSettings("claude", { env });
Expand All @@ -112,7 +112,10 @@ async function claudeSelectModel(modelType, port) {
const apiKey = await getFirstApiKey();
env.ANTHROPIC_BASE_URL = endpoint;
env.API_TIMEOUT_MS = "600000";
if (apiKey) env.ANTHROPIC_AUTH_TOKEN = apiKey;
if (apiKey) {
env.ANTHROPIC_AUTH_TOKEN = apiKey;
env.ANTHROPIC_API_KEY = apiKey;
}
}

const result = await api.applyCliToolSettings("claude", { env });
Expand Down
13 changes: 12 additions & 1 deletion cli/src/cli/menus/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ const PROVIDER_MODELS = {
{ id: "glm-4.6v" },
],
kimi: [
{ id: "kimi-for-coding" },
{ id: "kimi-k2.6" },
{ id: "kimi-k2.5" },
{ id: "kimi-k2.5-thinking" },
{ id: "kimi-latest" },
],
"kimi-api": [
{ id: "kimi-k2.6" },
{ id: "kimi-k2.5" },
{ id: "kimi-k2.5-thinking" },
{ id: "kimi-latest" },
],
minimax: [
Expand All @@ -128,7 +138,8 @@ 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 Code" },
"kimi-api": { id: "kimi-api", name: "Kimi API" },
openai: { id: "openai", name: "OpenAI" },
anthropic: { id: "anthropic", name: "Anthropic" },
gemini: { id: "gemini", name: "Gemini" },
Expand Down
5 changes: 3 additions & 2 deletions cli/src/cli/utils/modelSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { clearScreen } = require("./display");
// Provider alias order: OAuth first, then API Key (matches ModelSelectModal)
const PROVIDER_ALIAS_ORDER = [
"cc", "ag", "cx", "if", "qw", "gc", "gh", "kr",
"openrouter", "glm", "kimi", "minimax", "openai", "anthropic", "gemini"
"openrouter", "glm", "kimi", "kimi-api", "minimax", "openai", "anthropic", "gemini"
];

// Alias to display name mapping
Expand All @@ -20,7 +20,8 @@ const PROVIDER_ALIAS_NAMES = {
kr: "Kiro AI",
openrouter: "OpenRouter",
glm: "GLM Coding",
kimi: "Kimi Coding",
kimi: "Kimi Code",
"kimi-api": "Kimi API",
minimax: "Minimax Coding",
openai: "OpenAI",
anthropic: "Anthropic",
Expand Down
124 changes: 124 additions & 0 deletions cloud/src/handlers/embeddings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { getModelInfoCore } from "../../../open-sse/services/model.js";
import { handleEmbeddingsCore } from "../../../open-sse/handlers/embeddingsCore.js";
import { parseApiKey } from "../utils/apiKey.js";
import { getMachineData, saveMachineData } from "../services/storage.js";

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};

function jsonError(status, message, extraHeaders = {}) {
return new Response(JSON.stringify({ error: { message } }), {
status,
headers: { "Content-Type": "application/json", ...corsHeaders, ...extraHeaders },
});
}

function addCorsHeaders(response) {
const headers = new Headers(response.headers);
for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}

function bearerToken(request) {
const header = request.headers.get("Authorization") || "";
const match = header.match(/^Bearer\s+(.+)$/i);
return match ? match[1].trim() : null;
}

function selectCredentials(machineData, provider) {
const providers = machineData?.providers || {};
const now = Date.now();
const entries = Object.entries(providers)
.filter(([, account]) => account?.provider === provider && account?.isActive !== false)
.sort((a, b) => (a[1].priority || 999) - (b[1].priority || 999));

if (!entries.length) return { error: "No credentials for provider" };

const available = entries.find(([, account]) => {
if (!account.rateLimitedUntil) return true;
return new Date(account.rateLimitedUntil).getTime() <= now;
});

if (available) {
const [connectionId, account] = available;
return {
credentials: {
...account,
connectionId,
},
};
}

const retryAt = Math.min(...entries.map(([, account]) => new Date(account.rateLimitedUntil).getTime()).filter(Number.isFinite));
const retryAfter = Math.max(1, Math.ceil((retryAt - now) / 1000));
return { rateLimited: true, retryAfter };
}

export async function handleEmbeddings(request, env, ctx, machineIdOverride = null) {
if (request.method === "OPTIONS") {
return new Response(null, { status: 200, headers: corsHeaders });
}

const token = bearerToken(request);
if (!token) return jsonError(401, "Missing API key");

let machineId = machineIdOverride;
if (!machineId) {
const parsed = await parseApiKey(token, env);
if (!parsed) return jsonError(401, "Invalid API key format");
if (!parsed.machineId || parsed.isNewFormat === false) {
return jsonError(400, "Use the machineId endpoint for this API key");
}
machineId = parsed.machineId;
}

const machineData = await getMachineData(env, machineId);
const validKey = machineData?.apiKeys?.some((entry) => entry?.key === token);
if (!validKey) return jsonError(401, "Invalid API key");

let body;
try {
body = await request.json();
} catch {
return jsonError(400, "Invalid JSON body");
}

if (!body?.model) return jsonError(400, "Missing model");
if (body.input === undefined || body.input === null) return jsonError(400, "Missing required field: input");

const modelInfo = await getModelInfoCore(body.model, machineData?.modelAliases);
if (!modelInfo?.provider || !modelInfo?.model) {
return jsonError(400, "Invalid model format");
}

const selected = selectCredentials(machineData, modelInfo.provider);
if (selected.error) return jsonError(400, selected.error);
if (selected.rateLimited) {
return jsonError(429, "All provider accounts are rate limited", {
"Retry-After": String(selected.retryAfter),
});
}

const result = await handleEmbeddingsCore({
body,
modelInfo,
credentials: selected.credentials,
onRequestSuccess: async () => {},
});

if (result.success) return addCorsHeaders(result.response);

if (result.status === 429) {
selected.credentials.rateLimitedUntil = new Date(Date.now() + 60000).toISOString();
await saveMachineData(env, machineId, machineData);
}

return addCorsHeaders(result.response || jsonError(result.status || 500, result.error || "Embedding request failed"));
}
5 changes: 5 additions & 0 deletions cloud/src/services/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function getMachineData() {
return null;
}

export async function saveMachineData() {}
9 changes: 9 additions & 0 deletions cloud/src/utils/apiKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function extractBearerToken(header) {
const value = typeof header === "string" ? header : "";
const match = value.match(/^Bearer\s+(.+)$/i);
return match ? match[1].trim() : null;
}

export async function parseApiKey() {
return null;
}
4 changes: 4 additions & 0 deletions cloud/src/utils/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function info() {}
export function debug() {}
export function warn() {}
export function error() {}
16 changes: 16 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@ import nextVitals from "eslint-config-next/core-web-vitals";

const eslintConfig = defineConfig([
...nextVitals,
{
rules: {
"react-hooks/set-state-in-effect": "off",
"react-hooks/immutability": "off",
"react-hooks/purity": "off",
"react-hooks/refs": "off",
"react-hooks/exhaustive-deps": "off",
"import/no-anonymous-default-export": "off",
"@next/next/no-img-element": "off",
},
},
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
".next-cli-build/**",
"out/**",
"build/**",
"logs/**",
"output/**",
".playwright-cli/**",
"next-env.d.ts",
"gitbook/**",
]),
]);

Expand Down
7 changes: 7 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const nextConfig = {
path: false,
};
}
config.ignoreWarnings = [
...(config.ignoreWarnings || []),
{
module: /browserslist[\\/]node\.js/,
message: /Critical dependency: require function is used/,
},
];
// Exclude logs, .next, gitbook subapp from watcher
config.watchOptions = { ...config.watchOptions, ignored: /[\\/](logs|\.next|gitbook|cli)[\\/]/ };
return config;
Expand Down
6 changes: 5 additions & 1 deletion open-sse/config/providerModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ export const PROVIDER_MODELS = {
{ id: "gpt-5.3-codex", name: "GPT 5.3 Codex" },
],
kmc: [ // Kimi Coding
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "kimi-for-coding", name: "Kimi for Coding" },
{ id: "kimi-k2.6", name: "Kimi K2.6", upstreamModelId: "kimi-for-coding" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" },
{ id: "kimi-latest", name: "Kimi Latest" },
Expand Down Expand Up @@ -323,6 +324,9 @@ export const PROVIDER_MODELS = {
{ id: "glm-4.5-air", name: "GLM-4.5-Air" },
],
kimi: [
{ id: "kimi-k2.6", name: "Kimi K2.6 (Kimi Code)", upstreamModelId: "kimi-for-coding" },
],
"kimi-api": [
{ 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" },
Expand Down
29 changes: 26 additions & 3 deletions open-sse/config/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ const CLAUDE_CLI_SPOOF_HEADERS = {

// Shared baseUrls
const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/v1/messages";
const KIMI_CODING_OPENAI_BASE_URL = "https://api.kimi.com/coding/v1/chat/completions";
const KIMI_CODING_MODELS_URL = "https://api.kimi.com/coding/v1/models";
const KIMI_API_BASE_URLS = [
"https://api.moonshot.ai/v1/chat/completions",
"https://api.moonshot.cn/v1/chat/completions",
];
const KIMI_API_MODELS_URLS = [
"https://api.moonshot.ai/v1/models",
"https://api.moonshot.cn/v1/models",
];

export const PROVIDERS = {
claude: {
Expand Down Expand Up @@ -139,9 +149,22 @@ export const PROVIDERS = {
headers: {}
},
kimi: {
baseUrl: KIMI_CODING_BASE_URL,
format: "claude",
headers: { ...CLAUDE_API_HEADERS }
baseUrls: [KIMI_CODING_OPENAI_BASE_URL],
baseUrl: KIMI_CODING_OPENAI_BASE_URL,
openaiBaseUrl: KIMI_CODING_OPENAI_BASE_URL,
anthropicBaseUrl: KIMI_CODING_BASE_URL,
modelsUrl: KIMI_CODING_MODELS_URL,
modelsUrls: [KIMI_CODING_MODELS_URL],
format: "openai",
headers: {}
},
"kimi-api": {
baseUrls: KIMI_API_BASE_URLS,
baseUrl: KIMI_API_BASE_URLS[0],
modelsUrls: KIMI_API_MODELS_URLS,
modelsUrl: KIMI_API_MODELS_URLS[0],
format: "openai",
headers: {}
},
minimax: {
baseUrl: "https://api.minimax.io/anthropic/v1/messages",
Expand Down
Loading