Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2388872
feat: add kimi alias support and stabilize slides output
spirosrap Feb 14, 2026
18e56bc
feat: add kimi alias support and stabilize slides output
spirosrap Feb 14, 2026
8a6d15b
fix: repair provider wiring after rebase conflict resolution
spirosrap Feb 14, 2026
995d47f
Merge remote-tracking branch 'fork/feat/minimax-provider-alias' into …
spirosrap Feb 14, 2026
dd12078
fix: restore daemon model resolution after merge
spirosrap Feb 14, 2026
8a4a4cd
fix: reduce mid-sentence slide truncation with transcript fallback
spirosrap Feb 14, 2026
b67a7f7
fix: avoid large transcript blob injection in slide summaries
spirosrap Feb 14, 2026
8c125d4
fix: compact oversized slide narratives without title duplication
spirosrap Feb 14, 2026
ae6e226
fix: enforce concise third-person slide summaries
spirosrap Feb 14, 2026
a0a8c3f
tune: increase per-slide summary depth by length preset
spirosrap Feb 14, 2026
46876c2
fix: suppress transcript-style slide fallback injection
spirosrap Feb 14, 2026
4cec1dc
fix: clean transcript-style pronoun rewrite artifacts
spirosrap Feb 14, 2026
5f11232
fix: normalize conversational slide tails into summary prose
spirosrap Feb 14, 2026
999bf7e
fix: catch disfluency-led slide tails and strip single-bullet monologues
spirosrap Feb 14, 2026
7ac0f2d
slides: strip leading disfluencies in transcript rewrite
spirosrap Feb 15, 2026
bfd039e
slides: clean disfluency and stutter tails
spirosrap Feb 15, 2026
bc7df74
slides: fix transcript-tail grammar artifacts
spirosrap Feb 15, 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
91 changes: 91 additions & 0 deletions PR_DRAFT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Title
Add first-class Kimi model support + stabilize slides/progress output

## Summary
This PR improves model/provider ergonomics and fixes multiple output stability issues seen in slides mode.

### 1) First-class Kimi support (similar to MiniMax)
- Add `--model kimi` alias (defaults to `openai/kimi-k2.5`)
- Add `--model kimi/<model>`
- Add provider env support:
- `KIMI_API_KEY` (primary)
- `MOONSHOT_API_KEY` (alias)
- `KIMI_BASE_URL` (primary)
- `MOONSHOT_BASE_URL` (alias)
- default base URL: `https://api.moonshot.ai/v1`
- Wire Kimi handling through:
- model parsing/spec
- auto model candidate resolution
- env/runtime context
- URL and asset flows
- daemon agent/flow context
- help/docs

### 2) Kimi runtime compatibility fix
- Kimi rejects custom temperatures in this integration (`only 1 is allowed`).
- For `openai/kimi...` models, force effective `temperature=1` to avoid runtime failures.

### 3) Slides/progress output stabilization
- Fix progress gate behavior so spinner/progress rendering does not race against stdout output.
- Improve slides streaming/marker handling to avoid dropped interleaved text in chunked output.
- Preserve deterministic text output in non-inline terminals (fallback path still prints slide paths in debug/non-inline contexts).

### 4) Slide text fallback quality
- Improve transcript fallback segmentation for slide blocks:
- reduce clipped starts
- better boundary handling for continuation fragments
- avoid over-aggressive replacement of long narrative bodies
- Add regression coverage around truncated/ellipsis and chunk boundary behavior.

## Why
Users reported:
- Needing cumbersome env remapping to use non-OpenAI-compatible providers.
- `--model minimax`/provider aliases not being symmetrical/extensible.
- Slides output truncation/misalignment and occasional paragraph clipping.
- Kimi calls failing due to temperature constraints.

This PR addresses those pain points directly.

## User-facing behavior changes
- New:
- `summarize "<url>" --model kimi`
- `summarize "<url>" --model kimi/<model-id>`
- Existing workflows remain supported.
- `--verbose` env diagnostics now include `kimiKey=true|false`.

## Docs
- `README.md`
- `docs/llm.md`
- `docs/config.md`
- CLI help text (`src/run/help.ts`)

## Tests
Added/updated coverage for:
- model spec parsing (`kimi`, `kimi/<model>`)
- auto model candidate support for Kimi aliases
- config env legacy key mapping (`apiKeys.kimi`)
- daemon agent key selection for Kimi
- temperature handling for Kimi
- slides text/stream/progress regressions

## Validation run
```bash
corepack pnpm exec vitest run \
tests/model-spec.test.ts \
tests/model-auto.test.ts \
tests/config.env.test.ts \
tests/daemon.agent.test.ts \
tests/llm.generate-text.test.ts \
tests/slides-output.stream-handler.test.ts \
tests/slides-text.utils.test.ts \
tests/progress-gate.test.ts

corepack pnpm exec tsc -p tsconfig.build.json
corepack pnpm exec node scripts/build-cli.mjs
```

## Notes / follow-up
- There is a local scratch file `tmp-coerce-check.ts` in the working tree; this should not be included in the final PR unless intentionally needed.
- This branch includes both provider and slides/progress work in one PR. If preferred, it can be split into:
1. provider/model support (Kimi/MiniMax-related)
2. slides/progress stability fixes
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,14 @@ Notes:

### Model ids

Use gateway-style ids: `<provider>/<model>`.
Use gateway-style ids: `<provider>/<model>`. MiniMax and Kimi also support short aliases.

Examples:

- `minimax` (alias for MiniMax default model)
- `minimax/minimax-m2.5`
- `kimi` (alias for Kimi default model)
- `kimi/kimi-k2.5`
- `openai/gpt-5-mini`
- `anthropic/claude-sonnet-4-5`
- `xai/grok-4-fast-non-reasoning`
Expand Down Expand Up @@ -552,7 +556,11 @@ Set the key matching your chosen `--model`:
- legacy `"apiKeys"` still works (mapped to env names)

- `OPENAI_API_KEY` (for `openai/...`)
- `NVIDIA_API_KEY` (for `nvidia/...`)
- `NVIDIA_API_KEY` (for `nvidia/...`; supports `NGC_API_KEY` alias)
- `MINIMAX_API_KEY` (for `minimax` and `minimax/...`)
- `MINIMAX_BASE_URL` (optional MiniMax base URL override, default `https://api.minimax.io/v1`)
- `KIMI_API_KEY` (for `kimi` and `kimi/...`; supports `MOONSHOT_API_KEY` alias)
- `KIMI_BASE_URL` (optional Kimi base URL override, default `https://api.moonshot.ai/v1`; supports `MOONSHOT_BASE_URL` alias)
- `ANTHROPIC_API_KEY` (for `anthropic/...`)
- `XAI_API_KEY` (for `xai/...`)
- `Z_AI_API_KEY` (for `zai/...`; supports `ZAI_API_KEY` alias)
Expand Down Expand Up @@ -636,6 +644,16 @@ Z.AI (OpenAI-compatible):
- `Z_AI_API_KEY=...` (or `ZAI_API_KEY=...`)
- Optional base URL override: `Z_AI_BASE_URL=...`

MiniMax (OpenAI-compatible):

- `MINIMAX_API_KEY=...`
- Optional base URL override: `MINIMAX_BASE_URL=...`

Kimi (OpenAI-compatible):

- `KIMI_API_KEY=...` (or `MOONSHOT_API_KEY=...`)
- Optional base URL override: `KIMI_BASE_URL=...` (or `MOONSHOT_BASE_URL=...`)

Optional services:

- `FIRECRAWL_API_KEY` (website extraction fallback)
Expand Down
2 changes: 2 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Legacy shortcut (still supported):
{
"apiKeys": {
"openai": "sk-...",
"minimax": "...",
"kimi": "...",
"anthropic": "sk-ant-...",
"google": "...",
"openrouter": "sk-or-...",
Expand Down
18 changes: 18 additions & 0 deletions docs/llm.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ installed, auto mode can use local CLI models via `cli.enabled` or implicit auto
- `OPENAI_USE_CHAT_COMPLETIONS` (optional; force OpenAI chat completions)
- `NVIDIA_API_KEY` (required for `nvidia/...` models; alias: `NGC_API_KEY`)
- `NVIDIA_BASE_URL` (optional; override NVIDIA OpenAI-compatible API endpoint; default: `https://integrate.api.nvidia.com/v1`)
- `MINIMAX_API_KEY` (required for `minimax` and `minimax/...` models)
- `MINIMAX_BASE_URL` (optional; override MiniMax base URL, default `https://api.minimax.io/v1`)
- `KIMI_API_KEY` (required for `kimi` and `kimi/...` models; also accepts `MOONSHOT_API_KEY`)
- `KIMI_BASE_URL` (optional; override Kimi base URL, default `https://api.moonshot.ai/v1`; also accepts `MOONSHOT_BASE_URL`)
- `OPENROUTER_API_KEY` (optional; required for `openrouter/...` models; also used when `OPENAI_BASE_URL` points to OpenRouter)
- `Z_AI_API_KEY` (required for `zai/...` models; supports `ZAI_API_KEY` alias)
- `Z_AI_BASE_URL` (optional; override default Z.AI base URL)
Expand All @@ -47,6 +51,10 @@ installed, auto mode can use local CLI models via `cli.enabled` or implicit auto
- `google/gemini-3-flash-preview`
- `openai/gpt-5-mini`
- `nvidia/z-ai/glm5`
- `minimax`
- `minimax/minimax-m2.5`
- `kimi`
- `kimi/kimi-k2.5`
- `zai/glm-4.7`
- `xai/grok-4-fast-non-reasoning`
- `google/gemini-2.0-flash`
Expand Down Expand Up @@ -98,6 +106,16 @@ installed, auto mode can use local CLI models via `cli.enabled` or implicit auto

Use `--model zai/<model>` (e.g. `zai/glm-4.7`). Defaults to Z.AI’s base URL and uses chat completions.

## MiniMax

Use `--model minimax` (default MiniMax model) or `--model minimax/<model>`.
Requires `MINIMAX_API_KEY`. Requests use MiniMax's OpenAI-compatible endpoint by default.

## Kimi

Use `--model kimi` (default Kimi model) or `--model kimi/<model>`.
Requires `KIMI_API_KEY` (or `MOONSHOT_API_KEY`). Requests use Kimi's OpenAI-compatible endpoint by default.

## Input limits

- Text prompts are checked against the model’s max input tokens (LiteLLM catalog) using a GPT tokenizer.
Expand Down
23 changes: 22 additions & 1 deletion packages/core/src/prompts/link-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,26 @@ export function buildLinkSummaryPrompt({
const listGuidanceLine =
"Use short paragraphs; use bullet lists only when they improve scanability; avoid rigid templates.";
const quoteGuidanceLine =
"Include 1-2 short exact excerpts (max 25 words each) formatted as Markdown italics using single asterisks when there is a strong, non-sponsor line. Use straight quotation marks (no curly) as needed. If no suitable line exists, omit excerpts. Never include ad/sponsor/boilerplate excerpts and do not mention them.";
slides && slides.count > 0
? "For slide blocks, do not include direct quotes or transcript excerpts."
: "Include 1-2 short exact excerpts (max 25 words each) formatted as Markdown italics using single asterisks when there is a strong, non-sponsor line. Use straight quotation marks (no curly) as needed. If no suitable line exists, omit excerpts. Never include ad/sponsor/boilerplate excerpts and do not mention them.";
const slideSentenceGuidance = (() => {
if (!(slides && slides.count > 0)) return "";
if (preset === "short") return "For each slide block, write a concise neutral summary (2-3 sentences).";
if (preset === "medium") return "For each slide block, write a concise neutral summary (2-4 sentences).";
if (preset === "long") return "For each slide block, write a concise neutral summary (3-5 sentences).";
if (preset === "xl") return "For each slide block, write a concise neutral summary (3-6 sentences).";
return "For each slide block, write a concise neutral summary (4-7 sentences).";
})();
const slideToneInstruction =
slides && slides.count > 0
? [
slideSentenceGuidance,
"Use third-person prose; avoid first-person/second-person voice and direct address.",
"Paraphrase transcript content; do not copy long conversational passages.",
"Exclude banter, filler phrases, and rhetorical asides.",
].join("\n")
: "";
const sponsorInstruction =
hasTranscript || (slides && slides.count > 0)
? "Omit sponsor messages, ads, promos, and calls-to-action (including podcast ad reads), even if they appear in the transcript or slide timeline. Do not mention or acknowledge them, and do not say you skipped or ignored anything. Avoid sponsor/ad/promo language, brand names like Squarespace, or CTA phrases like discount code. Treat them as if they do not exist. If a slide segment is purely sponsor/ad content, leave that slide marker with no text."
Expand All @@ -222,9 +241,11 @@ export function buildLinkSummaryPrompt({
"Format the answer in Markdown and obey the length-specific formatting above.",
listGuidanceLine,
quoteGuidanceLine,
slideToneInstruction,
"Base everything strictly on the provided content and never invent details.",
"Final check: remove any sponsor/ad references or mentions of skipping/ignoring content. Ensure excerpts (if any) are italicized and use only straight quotes.",
'Final check for slides: every [slide:N] must be immediately followed by a line that starts with "## ". Remove any "Title:" or "Slide" label lines.',
"Final check for slides: rewrite transcript-like direct speech into concise neutral summaries.",
timestampInstruction,
shareGuidance,
slideInstruction,
Expand Down
8 changes: 7 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export type NvidiaConfig = {
export type ApiKeysConfig = {
openai?: string;
nvidia?: string;
minimax?: string;
kimi?: string;
anthropic?: string;
google?: string;
xai?: string;
Expand Down Expand Up @@ -131,7 +133,7 @@ export type AutoRule = {
/**
* Candidate model ids (ordered).
*
* - Native: `openai/...`, `google/...`, `xai/...`, `anthropic/...`, `zai/...`
* - Native: `openai/...`, `minimax`, `minimax/...`, `kimi`, `kimi/...`, `google/...`, `xai/...`, `anthropic/...`, `zai/...`
* - OpenRouter (forced): `openrouter/<provider>/<model>` (e.g. `openrouter/openai/gpt-5-mini`)
*/
candidates?: string[];
Expand Down Expand Up @@ -245,6 +247,8 @@ function resolveLegacyApiKeysEnv(apiKeys: ApiKeysConfig | undefined): EnvConfig
const mapped: EnvConfig = {};
if (typeof apiKeys.openai === "string") mapped.OPENAI_API_KEY = apiKeys.openai;
if (typeof apiKeys.nvidia === "string") mapped.NVIDIA_API_KEY = apiKeys.nvidia;
if (typeof apiKeys.minimax === "string") mapped.MINIMAX_API_KEY = apiKeys.minimax;
if (typeof apiKeys.kimi === "string") mapped.KIMI_API_KEY = apiKeys.kimi;
if (typeof apiKeys.anthropic === "string") mapped.ANTHROPIC_API_KEY = apiKeys.anthropic;
if (typeof apiKeys.google === "string") mapped.GEMINI_API_KEY = apiKeys.google;
if (typeof apiKeys.xai === "string") mapped.XAI_API_KEY = apiKeys.xai;
Expand Down Expand Up @@ -1194,6 +1198,8 @@ export function loadSummarizeConfig({ env }: { env: Record<string, string | unde
const allowed = [
"openai",
"nvidia",
"minimax",
"kimi",
"anthropic",
"google",
"xai",
Expand Down
79 changes: 79 additions & 0 deletions src/daemon/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,16 @@ type AgentApiKeys = {
xaiApiKey: string | null;
zaiApiKey: string | null;
nvidiaApiKey: string | null;
minimaxApiKey: string | null;
kimiApiKey: string | null;
};

const REQUIRED_ENV_BY_PROVIDER: Record<string, string> = {
openrouter: "OPENROUTER_API_KEY",
openai: "OPENAI_API_KEY",
nvidia: "NVIDIA_API_KEY",
minimax: "MINIMAX_API_KEY",
kimi: "KIMI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GEMINI_API_KEY",
xai: "XAI_API_KEY",
Expand All @@ -423,6 +427,10 @@ function resolveApiKeyForModel({
return apiKeys.openaiApiKey;
case "nvidia":
return apiKeys.nvidiaApiKey;
case "minimax":
return apiKeys.minimaxApiKey;
case "kimi":
return apiKeys.kimiApiKey;
case "anthropic":
return apiKeys.anthropicApiKey;
case "google":
Expand Down Expand Up @@ -463,6 +471,10 @@ async function resolveAgentModel({
googleApiKey,
xaiApiKey,
zaiApiKey,
minimaxApiKey,
minimaxBaseUrl,
kimiApiKey,
kimiBaseUrl,
providerBaseUrls,
zaiBaseUrl,
nvidiaApiKey,
Expand All @@ -487,6 +499,8 @@ async function resolveAgentModel({
xaiApiKey,
zaiApiKey,
nvidiaApiKey,
minimaxApiKey,
kimiApiKey,
};

const overrides = resolveRunOverrides({});
Expand Down Expand Up @@ -530,6 +544,39 @@ async function resolveAgentModel({
return { ...resolved, maxOutputTokens, apiKeys };
}

if (requestedModel.requiredEnv === "MINIMAX_API_KEY") {
const parsed = parseProviderModelId(requestedModel.llmModelId);
return {
provider: "openai",
model: resolveModelWithFallback({
provider: "openai",
modelId: parsed.model,
baseUrl: minimaxBaseUrl,
}),
maxOutputTokens,
apiKeys: {
...apiKeys,
openaiApiKey: minimaxApiKey,
},
};
}
if (requestedModel.requiredEnv === "KIMI_API_KEY") {
const parsed = parseProviderModelId(requestedModel.llmModelId);
return {
provider: "openai",
model: resolveModelWithFallback({
provider: "openai",
modelId: parsed.model,
baseUrl: kimiBaseUrl,
}),
maxOutputTokens,
apiKeys: {
...apiKeys,
openaiApiKey: kimiApiKey,
},
};
}

const { provider, model } = parseProviderModelId(requestedModel.userModelId);
const resolved = applyBaseUrlOverride(provider, model);
return { ...resolved, maxOutputTokens, apiKeys };
Expand All @@ -555,6 +602,38 @@ async function resolveAgentModel({
for (const attempt of attempts) {
if (attempt.transport === "cli") continue;
if (!envHasKey(envForAuto, attempt.requiredEnv)) continue;
if (attempt.requiredEnv === "MINIMAX_API_KEY") {
const parsed = parseProviderModelId(attempt.llmModelId ?? attempt.userModelId);
return {
provider: "openai",
model: resolveModelWithFallback({
provider: "openai",
modelId: parsed.model,
baseUrl: minimaxBaseUrl,
}),
maxOutputTokens,
apiKeys: {
...apiKeys,
openaiApiKey: minimaxApiKey,
},
};
}
if (attempt.requiredEnv === "KIMI_API_KEY") {
const parsed = parseProviderModelId(attempt.llmModelId ?? attempt.userModelId);
return {
provider: "openai",
model: resolveModelWithFallback({
provider: "openai",
modelId: parsed.model,
baseUrl: kimiBaseUrl,
}),
maxOutputTokens,
apiKeys: {
...apiKeys,
openaiApiKey: kimiApiKey,
},
};
}
if (attempt.transport === "openrouter") {
const modelId = attempt.userModelId.replace(/^openrouter\//i, "");
const resolved = applyBaseUrlOverride("openrouter", modelId);
Expand Down
Loading