feat: DeepSeek API integration + Whale lift (hooks, tool repair, MCP permissions, token tracking)

DeepSeek API:
- @ai-sdk/deepseek provider replaces openai-compatible for deepseek-* models
- Token tracking: cache_hit/reasoning tokens flow API → DB → WS frames → UI
- thinking effort levels (off/low/medium/high/xhigh/max) via AGENTS.md frontmatter
- V4 models: deepseek-v4-flash, deepseek-v4-pro
- Wired for both chat and coder panes

Whale lifts:
- Tool input repair (schema-based type coercion, markdown link unwrapping)
- Hooks system (6 lifecycle events, shell exec, JSON stdin/stdout contract)
- Per-MCP-server permissions (allow/ask/deny)
- token tracking UI (cache N, think N in message stats line)

Infra:
- New DB columns: messages.cache_tokens, messages.reasoning_tokens
- New WS frame fields: cache_tokens, reasoning_tokens on message_complete
- coder provider snapshot merges DeepSeek models alongside llama-swap
This commit is contained in:
2026-06-08 01:24:23 +00:00
parent c11e26090f
commit 203cfd2fa8
29 changed files with 916 additions and 42 deletions

View File

@@ -29,6 +29,22 @@ interface AgentRow {
last_probed_at: string | Date | null;
}
export async function fetchDeepSeekModels(config: Config): Promise<ProviderModel[]> {
if (!config.DEEPSEEK_API_KEY) return [];
try {
const baseURL = (config.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com').replace(/\/+$/, '');
const res = await fetch(`${baseURL}/v1/models`, {
headers: { Authorization: `Bearer ${config.DEEPSEEK_API_KEY}` },
signal: AbortSignal.timeout(5_000),
});
if (!res.ok) return [];
const parsed = (await res.json()) as { data?: Array<{ id: string }> };
return (parsed.data ?? []).map((m) => ({ id: m.id, label: m.id }));
} catch {
return [];
}
}
export async function fetchLlamaSwapModels(config: Config): Promise<ProviderModel[]> {
try {
const res = await fetch(`${config.LLAMA_SWAP_URL}/v1/models`);
@@ -256,7 +272,13 @@ export async function getProviderSnapshot(
}
const build = async (): Promise<ProviderSnapshotEntry[]> => {
const llamaModels = await fetchLlamaSwapModels(config);
const [llamaModels, deepseekModels] = await Promise.all([
fetchLlamaSwapModels(config),
fetchDeepSeekModels(config),
]);
// Merge DeepSeek models into the llama-swap model pool so the boocode
// provider (which sources from llama-swap) also includes DeepSeek models.
const mergedModels = mergeModels(llamaModels, deepseekModels);
const agents = await sql<AgentRow[]>`
SELECT name, install_path, supports_acp, models, commands, label, transport, last_probed_at FROM available_agents
`;
@@ -265,7 +287,7 @@ export async function getProviderSnapshot(
const entries = await Promise.all(
[...getResolvedRegistry().values()].map((resolved) =>
buildProviderEntry(resolved, agentMap.get(resolved.id), llamaModels, resolvedCwd, ttlMs, force),
buildProviderEntry(resolved, agentMap.get(resolved.id), mergedModels, resolvedCwd, ttlMs, force),
),
);