chore: init main branch
This commit is contained in:
commit
549c8b5b13
231 files changed
+27286
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import type { AgentSettings } from "../types";
|
||||
|
||||
const DEFAULT_SETTINGS: AgentSettings = {
|
||||
thinking: {
|
||||
enabled: true,
|
||||
effort: "medium",
|
||||
keep: "all",
|
||||
},
|
||||
loop_control: {
|
||||
max_retries_per_step: 3,
|
||||
reserved_context_size: 50000,
|
||||
},
|
||||
background: {
|
||||
keep_alive_on_exit: false,
|
||||
},
|
||||
permission: { rules: [] },
|
||||
hooks: [],
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getSection<T>(rawOther: unknown, key: string): T | undefined {
|
||||
const root = asRecord(rawOther);
|
||||
const section = root[key];
|
||||
if (section === undefined || section === null) return undefined;
|
||||
return section as T;
|
||||
}
|
||||
|
||||
export function getAgentSettings(rawOther: unknown): AgentSettings {
|
||||
return {
|
||||
thinking: {
|
||||
...DEFAULT_SETTINGS.thinking,
|
||||
...getSection<AgentSettings["thinking"]>(rawOther, "thinking"),
|
||||
},
|
||||
loop_control: {
|
||||
...DEFAULT_SETTINGS.loop_control,
|
||||
...getSection<AgentSettings["loop_control"]>(rawOther, "loop_control"),
|
||||
},
|
||||
background: {
|
||||
...DEFAULT_SETTINGS.background,
|
||||
...getSection<AgentSettings["background"]>(rawOther, "background"),
|
||||
},
|
||||
permission: {
|
||||
rules:
|
||||
getSection<AgentSettings["permission"]>(rawOther, "permission")?.rules ??
|
||||
[],
|
||||
},
|
||||
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export function setAgentSettings(
|
||||
rawOther: unknown,
|
||||
patch: Partial<AgentSettings>
|
||||
): unknown {
|
||||
const root = { ...asRecord(rawOther) };
|
||||
const current = getAgentSettings(rawOther);
|
||||
const next: AgentSettings = {
|
||||
thinking: { ...current.thinking, ...patch.thinking },
|
||||
loop_control: { ...current.loop_control, ...patch.loop_control },
|
||||
background: { ...current.background, ...patch.background },
|
||||
permission: {
|
||||
rules: patch.permission?.rules ?? current.permission?.rules ?? [],
|
||||
},
|
||||
hooks: patch.hooks ?? current.hooks ?? [],
|
||||
};
|
||||
|
||||
if (next.thinking) root.thinking = next.thinking;
|
||||
if (next.loop_control) root.loop_control = next.loop_control;
|
||||
if (next.background) root.background = next.background;
|
||||
if (next.permission?.rules && next.permission.rules.length > 0) {
|
||||
root.permission = next.permission;
|
||||
} else {
|
||||
delete root.permission;
|
||||
}
|
||||
if (next.hooks && next.hooks.length > 0) {
|
||||
root.hooks = next.hooks;
|
||||
} else {
|
||||
delete root.hooks;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Formatting helpers for the dashboard — ported from kimicode-dashboard/web/src/format.js
|
||||
|
||||
export function fmtInt(n: number, locale = "en"): string {
|
||||
return new Intl.NumberFormat(locale === "zh" ? "zh-CN" : "en-US").format(
|
||||
Math.round(Number(n) || 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function fmtTokens(n: number): string {
|
||||
const v = Number(n) || 0;
|
||||
if (v >= 1e9) return (v / 1e9).toFixed(2) + "B";
|
||||
if (v >= 1e6) return (v / 1e6).toFixed(2) + "M";
|
||||
if (v >= 1e3) return (v / 1e3).toFixed(1) + "K";
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
||||
export function fmtUsd(n: number): string {
|
||||
const v = Number(n) || 0;
|
||||
if (v === 0) return "$0.00";
|
||||
if (v < 0.01) return "$" + v.toFixed(4);
|
||||
if (v < 1) return "$" + v.toFixed(3);
|
||||
return "$" + v.toFixed(2);
|
||||
}
|
||||
|
||||
export function fmtPct(n: number): string {
|
||||
return ((Number(n) || 0) * 100).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
export function fmtTime(ms: number, locale = "en"): string {
|
||||
if (!ms) return "—";
|
||||
return new Date(ms).toLocaleString(locale === "zh" ? "zh-CN" : "en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
export const DEFAULT_MAX_CONTEXT_SIZE = 256000;
|
||||
|
||||
interface ModelContextRule {
|
||||
pattern: RegExp;
|
||||
max_context_size: number;
|
||||
}
|
||||
|
||||
const MODEL_CONTEXT_RULES: ModelContextRule[] = [
|
||||
// Kimi
|
||||
{ pattern: /^kimi-for-coding$/i, max_context_size: 262144 },
|
||||
{ pattern: /^kimi-k2\.5/i, max_context_size: 256000 },
|
||||
{ pattern: /^kimi-k2/i, max_context_size: 256000 },
|
||||
{ pattern: /^kimi-/i, max_context_size: 256000 },
|
||||
// GLM
|
||||
{ pattern: /^glm-5\.2/i, max_context_size: 1000000 },
|
||||
{ pattern: /^glm-5\.1/i, max_context_size: 256000 },
|
||||
{ pattern: /^glm-5/i, max_context_size: 256000 },
|
||||
{ pattern: /^glm-4/i, max_context_size: 128000 },
|
||||
{ pattern: /^glm-/i, max_context_size: 128000 },
|
||||
// MiniMax
|
||||
{ pattern: /^MiniMax-M3/i, max_context_size: 1000000 },
|
||||
{ pattern: /^MiniMax-Text-01/i, max_context_size: 400000 },
|
||||
{ pattern: /^MiniMax-/i, max_context_size: 256000 },
|
||||
// Qwen
|
||||
{ pattern: /^qwen2\.5/i, max_context_size: 128000 },
|
||||
{ pattern: /^qwen-max/i, max_context_size: 128000 },
|
||||
{ pattern: /^qwen-plus/i, max_context_size: 128000 },
|
||||
{ pattern: /^qwen-turbo/i, max_context_size: 128000 },
|
||||
{ pattern: /^qwen-coder/i, max_context_size: 128000 },
|
||||
{ pattern: /^qwen-/i, max_context_size: 128000 },
|
||||
// DeepSeek
|
||||
{ pattern: /^deepseek-r1/i, max_context_size: 64000 },
|
||||
{ pattern: /^deepseek-v3/i, max_context_size: 64000 },
|
||||
{ pattern: /^deepseek-coder/i, max_context_size: 64000 },
|
||||
{ pattern: /^deepseek-/i, max_context_size: 64000 },
|
||||
// Hunyuan
|
||||
{ pattern: /^hunyuan-pro/i, max_context_size: 32000 },
|
||||
{ pattern: /^hunyuan-standard/i, max_context_size: 32000 },
|
||||
{ pattern: /^hunyuan-lite/i, max_context_size: 32000 },
|
||||
{ pattern: /^hunyuan-/i, max_context_size: 32000 },
|
||||
// Doubao
|
||||
{ pattern: /^doubao-pro/i, max_context_size: 128000 },
|
||||
{ pattern: /^doubao-lite/i, max_context_size: 128000 },
|
||||
{ pattern: /^doubao-vision/i, max_context_size: 128000 },
|
||||
{ pattern: /^doubao-/i, max_context_size: 128000 },
|
||||
// ERNIE
|
||||
{ pattern: /^ernie-4\.0/i, max_context_size: 128000 },
|
||||
{ pattern: /^ernie-3\.5/i, max_context_size: 128000 },
|
||||
{ pattern: /^ernie-speed/i, max_context_size: 128000 },
|
||||
{ pattern: /^ernie-lite/i, max_context_size: 128000 },
|
||||
{ pattern: /^ernie-/i, max_context_size: 128000 },
|
||||
// Spark
|
||||
{ pattern: /^spark-v4/i, max_context_size: 32000 },
|
||||
{ pattern: /^spark-v3\.5/i, max_context_size: 32000 },
|
||||
{ pattern: /^spark-pro/i, max_context_size: 32000 },
|
||||
{ pattern: /^spark-max/i, max_context_size: 32000 },
|
||||
{ pattern: /^spark-/i, max_context_size: 32000 },
|
||||
// SenseChat
|
||||
{ pattern: /^sensechat-/i, max_context_size: 128000 },
|
||||
// Baichuan
|
||||
{ pattern: /^baichuan-4/i, max_context_size: 128000 },
|
||||
{ pattern: /^baichuan-3/i, max_context_size: 128000 },
|
||||
{ pattern: /^baichuan-/i, max_context_size: 128000 },
|
||||
// Yi
|
||||
{ pattern: /^yi-/i, max_context_size: 128000 },
|
||||
// Claude
|
||||
{ pattern: /^claude-opus/i, max_context_size: 200000 },
|
||||
{ pattern: /^claude-sonnet/i, max_context_size: 200000 },
|
||||
{ pattern: /^claude-haiku/i, max_context_size: 200000 },
|
||||
{ pattern: /^claude-/i, max_context_size: 200000 },
|
||||
// OpenAI
|
||||
{ pattern: /^gpt-4\.1/i, max_context_size: 1047576 },
|
||||
{ pattern: /^gpt-4o/i, max_context_size: 128000 },
|
||||
{ pattern: /^gpt-4-turbo/i, max_context_size: 128000 },
|
||||
{ pattern: /^gpt-4-/i, max_context_size: 128000 },
|
||||
// Gemini
|
||||
{ pattern: /^gemini-2\.0-flash/i, max_context_size: 1048576 },
|
||||
{ pattern: /^gemini-1\.5-pro/i, max_context_size: 2097152 },
|
||||
{ pattern: /^gemini-1\.5-flash/i, max_context_size: 1048576 },
|
||||
{ pattern: /^gemini-/i, max_context_size: 1048576 },
|
||||
];
|
||||
|
||||
export function getDefaultMaxContextSize(modelId: string): number {
|
||||
for (const rule of MODEL_CONTEXT_RULES) {
|
||||
if (rule.pattern.test(modelId)) {
|
||||
return rule.max_context_size;
|
||||
}
|
||||
}
|
||||
return DEFAULT_MAX_CONTEXT_SIZE;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,57 @@
|
||||
import snapshot from "./models-dev.json";
|
||||
|
||||
/**
|
||||
* Compact per-model reference data from models.dev
|
||||
* (see scripts/fetch-models-dev.mjs).
|
||||
*/
|
||||
export interface ModelRef {
|
||||
name?: string;
|
||||
context?: number;
|
||||
reasoning?: boolean;
|
||||
tool_call?: boolean;
|
||||
structured_output?: boolean;
|
||||
image?: boolean;
|
||||
video?: boolean;
|
||||
}
|
||||
|
||||
const data: Record<string, ModelRef> = snapshot;
|
||||
|
||||
// models.dev keys are "<lab>/<model>" and may be mixed-case (e.g.
|
||||
// "minimax/MiniMax-M3"), while provider APIs return bare model ids
|
||||
// (e.g. "kimi-k2.5"). Index lowercase for tolerant matching.
|
||||
const byLowerKey: Record<string, ModelRef> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
byLowerKey[key.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up reference data for a provider model id.
|
||||
* Matches exact key first, then any "<lab>/<id>" suffix.
|
||||
* Returns undefined when nothing matches — callers fall back to
|
||||
* getDefaultMaxContextSize / manual capabilities.
|
||||
*/
|
||||
export function getModelRef(modelId: string): ModelRef | undefined {
|
||||
const id = modelId.trim().toLowerCase();
|
||||
if (!id) return undefined;
|
||||
const exact = byLowerKey[id];
|
||||
if (exact) return exact;
|
||||
const suffix = `/${id}`;
|
||||
for (const [key, value] of Object.entries(byLowerKey)) {
|
||||
if (key.endsWith(suffix)) return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map reference data to Kimi Code capability flags.
|
||||
* Multimodal (image_in/video_in) comes from models.dev input modalities;
|
||||
* always_thinking cannot be derived and stays manual.
|
||||
*/
|
||||
export function capabilitiesFromRef(ref: ModelRef): string[] {
|
||||
const caps: string[] = [];
|
||||
if (ref.reasoning) caps.push("thinking");
|
||||
if (ref.tool_call) caps.push("tool_use");
|
||||
if (ref.image) caps.push("image_in");
|
||||
if (ref.video) caps.push("video_in");
|
||||
return caps;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { Config, Model, ProviderType } from "../types";
|
||||
|
||||
/**
|
||||
* Provider types that must have a base_url set in order to function.
|
||||
* Anthropic and Vertexai intentionally have no default — they read the
|
||||
* agent's CLI environment / SDK defaults, so leaving base_url empty is
|
||||
* a valid (and the only) state.
|
||||
*/
|
||||
const TYPES_REQUIRING_BASE_URL: ReadonlySet<ProviderType> = new Set<ProviderType>([
|
||||
"openai",
|
||||
"openai_responses",
|
||||
"kimi",
|
||||
"google-genai",
|
||||
]);
|
||||
|
||||
export interface ProviderIssue {
|
||||
name: string;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect a config and report any enabled providers that are not ready to
|
||||
* use. Used by the save flow to warn the user before persisting an
|
||||
* incomplete state to config.toml / SQLite.
|
||||
*
|
||||
* "Incomplete" means any of:
|
||||
* - missing API key (and not a managed OAuth provider)
|
||||
* - missing base_url for provider types that need one
|
||||
* - zero configured models
|
||||
*
|
||||
* Disabled providers are skipped — the user explicitly turned them off.
|
||||
*/
|
||||
export function validateProviders(
|
||||
config: Config,
|
||||
modelIndex: Record<string, Model>,
|
||||
// The i18n `t` from useTranslation() is strictly typed to its known key
|
||||
// union; a generic (key: string) signature is the right shape for a
|
||||
// library-style helper that doesn't want to import the project's i18n
|
||||
// module directly.
|
||||
t: (key: string, vars?: Record<string, unknown>) => string,
|
||||
// When set, only inspect this single provider. Used when activating a
|
||||
// specific provider: sibling providers merely listed in the config
|
||||
// must not block the switch.
|
||||
target?: string,
|
||||
): ProviderIssue[] {
|
||||
const issues: ProviderIssue[] = [];
|
||||
for (const provider of Object.values(config.providers)) {
|
||||
if (target && provider.name !== target) continue;
|
||||
if (!provider.enabled) continue;
|
||||
const reasons: string[] = [];
|
||||
|
||||
const apiKey = provider.api_key?.trim();
|
||||
if (!apiKey && !provider.managed) {
|
||||
reasons.push(t("validationNoApiKey"));
|
||||
}
|
||||
|
||||
const baseUrl = provider.base_url?.trim();
|
||||
if (!baseUrl && TYPES_REQUIRING_BASE_URL.has(provider.provider_type)) {
|
||||
reasons.push(t("validationNoBaseUrl"));
|
||||
}
|
||||
|
||||
const providerModels = Object.values(modelIndex).filter(
|
||||
(m) => m.provider === provider.name,
|
||||
);
|
||||
if (providerModels.length === 0) {
|
||||
reasons.push(t("validationNoModels"));
|
||||
} else {
|
||||
// A model entry exists but its `model` field may be blank (e.g. the
|
||||
// user hit "+ 添加模型" in the edit page and saved before filling
|
||||
// in the upstream model id). Those still break the provider.
|
||||
const blankAliases = providerModels
|
||||
.filter((m) => !m.model || !m.model.trim())
|
||||
.map((m) => m.alias);
|
||||
if (blankAliases.length > 0) {
|
||||
reasons.push(
|
||||
t("validationBlankModels", { aliases: blankAliases.join(", ") }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (reasons.length > 0) {
|
||||
issues.push({ name: provider.name, reasons });
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
Reference in new issue
Block a user