fix(server): use only the configured models for manual metadata generation (#7611)

Manual metadata generation is a non-empty agents.metadataGeneration.providers
list, but the resolver always appended the built-in defaults and the current
agent selection after it. When a manual model failed, the prompt was retried
on providers the user never chose. Manual now resolves to exactly the
configured list; Automatic is unchanged.

Commit message and PR text generation swallowed failures without logging, so
a broken manual model was invisible. The git metadata generator now logs the
final failure with each attempt's provider, model and error, and the
structured generation loop receives the session logger for per-attempt
warnings, matching the branch name path.
This commit is contained in:
Mohamed Boudra
2026-10-22 14:36:22 +07:00
committed by GitHub
parent 4673ab1903
commit 4574142314
21 changed files with 300 additions and 77 deletions
+1 -3
View File
@@ -317,7 +317,7 @@ Paseo tools; otherwise the provider policy applies. Provider and global policy a
session is created, resumed, imported, or reloaded, so configuration changes affect the next
session rather than an already-running one.
`agents.metadataGeneration.providers` controls the preferred structured-generation fallback order for daemon-side metadata tasks such as commit messages, PR text, branch names, and generated agent titles. Entries are tried first in the configured order, then Paseo falls through to dynamically discovered defaults and finally the current selection when available.
`agents.metadataGeneration.providers` selects the models for daemon-side metadata tasks: commit messages, PR text, branch names, and generated titles. An empty list is Automatic; a non-empty list is Manual and only those models are tried. See [public-docs/metadata-generation.md](../public-docs/metadata-generation.md#how-a-model-is-chosen).
### Git process limits
@@ -353,8 +353,6 @@ Environment variables override `config.json`:
after changing `config.json`. Environment changes require a daemon restart; the launch environment
remains authoritative during reload.
`agents.metadataGeneration.providers` controls the preferred structured-generation fallback order for daemon-side metadata tasks such as commit messages, PR text, branch names, and generated agent titles. Entries are tried first in the configured order, then Paseo falls through to dynamically discovered defaults and finally the current selection when available.
Local speech model ids are intentionally narrow: STT uses `parakeet-tdt-0.6b-v2-int8`, TTS uses `kokoro-en-v0_19`, and turn detection uses the bundled Silero VAD model.
Set these to select OpenAI instead of local speech:
+1 -1
View File
@@ -2253,7 +2253,7 @@ export const ar: TranslationResources = {
automaticHint: "يختار Paseo نموذجًا سريعًا متاحًا",
preferredHint: "اختر النموذج الذي يستخدمه Paseo",
model: "النموذج",
fallbackHint: "إذا لم يكن متاحًا، يستخدم Paseo نموذجًا آخر متاحًا",
modelHint: "يُستخدم هذا النموذج فقط. إذا فشل، لا يجرّب Paseo نموذجًا آخر",
docs: "الوثائق",
saveError: "تعذر تحديث إنشاء البيانات الوصفية",
},
+1 -1
View File
@@ -2342,7 +2342,7 @@ export const en = {
automaticHint: "Paseo picks a fast available model",
preferredHint: "Choose the model Paseo uses",
model: "Model",
fallbackHint: "If it is unavailable, Paseo falls back to another available model",
modelHint: "Only this model is used. If it fails, Paseo does not try another model",
docs: "Docs",
saveError: "Unable to update metadata generation",
},
+1 -1
View File
@@ -2265,7 +2265,7 @@ export const es: TranslationResources = {
automaticHint: "Paseo elige un modelo rápido disponible",
preferredHint: "Elige el modelo que usa Paseo",
model: "Modelo",
fallbackHint: "Si no está disponible, Paseo usa otro modelo disponible",
modelHint: "Solo se usa este modelo. Si falla, Paseo no prueba otro modelo",
docs: "Documentación",
saveError: "No se pudo actualizar la generación de metadatos",
},
+1 -1
View File
@@ -2265,7 +2265,7 @@ export const fr: TranslationResources = {
automaticHint: "Paseo choisit un modèle rapide disponible",
preferredHint: "Choisissez le modèle utilisé par Paseo",
model: "Modèle",
fallbackHint: "S’il est indisponible, Paseo utilise un autre modèle disponible",
modelHint: "Seul ce modèle est utilisé. S’il échoue, Paseo n’essaie pas d’autre modèle",
docs: "Documentation",
saveError: "Impossible de mettre à jour la génération de métadonnées",
},
+1 -1
View File
@@ -2238,7 +2238,7 @@ export const ja: TranslationResources = {
automaticHint: "Paseo が利用可能な高速モデルを選択します",
preferredHint: "Paseo が使用するモデルを選択します",
model: "モデル",
fallbackHint: "利用できない場合、Paseo は別の利用可能なモデルを使用します",
modelHint: "このモデルのみを使用します。失敗しても、Paseo は別のモデルを試しません",
docs: "ドキュメント",
saveError: "メタデータ生成を更新できません",
},
+1 -1
View File
@@ -2227,7 +2227,7 @@ export const ko: TranslationResources = {
automaticHint: "Paseo가 사용 가능한 빠른 모델을 선택합니다",
preferredHint: "Paseo에서 사용할 모델을 선택하세요",
model: "모델",
fallbackHint: "사용할 수 없으면 Paseo가 다른 사용 가능한 모델을 사용합니다",
modelHint: "이 모델만 사용합니다. 실패해도 Paseo는 다른 모델을 시도하지 않습니다",
docs: "문서",
saveError: "메타데이터 생성을 업데이트할 수 없습니다",
},
+1 -1
View File
@@ -2250,7 +2250,7 @@ export const ptBR: TranslationResources = {
automaticHint: "O Paseo escolhe um modelo rápido disponível",
preferredHint: "Escolha o modelo que o Paseo usa",
model: "Modelo",
fallbackHint: "Se ele não estiver disponível, o Paseo usa outro modelo disponível",
modelHint: "Somente este modelo é usado. Se ele falhar, o Paseo não tenta outro modelo",
docs: "Documentação",
saveError: "Não foi possível atualizar a geração de metadados",
},
+2 -1
View File
@@ -2250,7 +2250,8 @@ export const ru: TranslationResources = {
automaticHint: "Paseo выбирает быструю доступную модель",
preferredHint: "Выберите модель, которую использует Paseo",
model: "Модель",
fallbackHint: "Если она недоступна, Paseo использует другую доступную модель",
modelHint:
"Используется только эта модель. Если она не сработает, Paseo не будет пробовать другую модель",
docs: "Документация",
saveError: "Не удалось обновить настройки генерации метаданных",
},
+1 -1
View File
@@ -2194,7 +2194,7 @@ export const zhCN: TranslationResources = {
automaticHint: "Paseo 会选择一个可用的快速模型",
preferredHint: "选择 Paseo 使用的模型",
model: "模型",
fallbackHint: "如果不可用,Paseo 会改用其他可用模型",
modelHint: "只使用此模型。如果失败,Paseo 不会尝试其他模型",
docs: "文档",
saveError: "无法更新元数据生成设置",
},
@@ -139,7 +139,7 @@ export function MetadataGenerationPage({ serverId }: { serverId: string }) {
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.metadataGeneration.model")}</Text>
<Text style={settingsStyles.rowHint}>
{t("settings.metadataGeneration.fallbackHint")}
{t("settings.metadataGeneration.modelHint")}
</Text>
</View>
<CombinedModelSelector
@@ -254,6 +254,57 @@ describe("generateStructuredAgentResponseWithFallback", () => {
expect(manager.checkedProviders).toEqual(["claude", "codex"]);
});
it("logs the provider, model and error of every failed attempt", async () => {
const warnings: Array<{ obj: object; msg?: string }> = [];
const runnerError = new Error("connection refused");
const manager = createManager([
{ provider: "local-llm", available: true, error: null },
{ provider: "opencode", available: false, error: "not installed" },
]);
await expect(
generateStructuredAgentResponseWithFallback({
manager,
cwd: "/tmp/project",
prompt: "Return JSON",
schema,
schemaName: "BranchName",
providers: [
{ provider: "local-llm", model: "qwen-local" },
{ provider: "opencode", model: "minimax-m3" },
],
logger: {
info: () => undefined,
warn: (obj, msg) => warnings.push({ obj, msg }),
},
runner: async () => {
throw runnerError;
},
}),
).rejects.toBeInstanceOf(StructuredAgentFallbackError);
expect(warnings).toEqual([
{
obj: {
err: runnerError,
provider: "local-llm",
model: "qwen-local",
schemaName: "BranchName",
},
msg: "Structured generation: provider failed",
},
{
obj: {
provider: "opencode",
model: "minimax-m3",
schemaName: "BranchName",
reason: "not installed",
},
msg: "Structured generation: skipping unavailable provider",
},
]);
});
it("throws a fallback error when all providers are unavailable or fail", async () => {
const manager = createManager([
{ provider: "claude", available: false, error: "missing auth" },
@@ -475,7 +475,7 @@ export async function generateStructuredAgentResponseWithFallback<T>(
});
logger?.warn(
{ err: error, provider: candidate.provider, model: candidate.model, schemaName },
"Structured generation: provider failed, trying next",
"Structured generation: provider failed",
);
}
}
@@ -18,7 +18,7 @@ class ProviderSnapshots {
}
describe("resolveStructuredGenerationProviders", () => {
test("tries the configured model before dynamically discovered fallbacks", async () => {
test("tries only the configured models when metadata generation is manual", async () => {
const snapshots = new ProviderSnapshots([
{
provider: "work-claude",
@@ -28,6 +28,12 @@ describe("resolveStructuredGenerationProviders", () => {
{ provider: "work-claude", id: "claude-haiku-2026", label: "Haiku", isDefault: true },
],
},
{
provider: "opencode",
status: READY,
enabled: true,
models: [{ provider: "opencode", id: "minimax-m3", label: "MiniMax M3", isDefault: true }],
},
]);
const providers = await resolveStructuredGenerationProviders({
@@ -35,14 +41,18 @@ describe("resolveStructuredGenerationProviders", () => {
providerSnapshotManager: snapshots,
daemonConfig: {
metadataGeneration: {
providers: [{ provider: "mock", model: "ten-second-stream" }],
providers: [
{ provider: "local-llm", model: "qwen-local" },
{ provider: "mock", model: "ten-second-stream" },
],
},
},
currentSelection: { provider: "opencode", model: "minimax-m3" },
});
expect(providers).toEqual([
{ provider: "local-llm", model: "qwen-local" },
{ provider: "mock", model: "ten-second-stream" },
{ provider: "work-claude", model: "claude-haiku-2026" },
]);
expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]);
});
@@ -227,10 +237,7 @@ describe("resolveStructuredGenerationProviders", () => {
},
});
expect(providers).toEqual([
{ provider: "current-provider", model: "configured-model" },
{ provider: "current-provider", model: "selected-model", thinkingOptionId: "medium" },
]);
expect(providers).toEqual([{ provider: "current-provider", model: "configured-model" }]);
expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]);
});
});
@@ -40,6 +40,10 @@ export interface ResolveStructuredGenerationProvidersOptions {
};
}
/**
* A non-empty configured list means Manual: only those models are tried, in order.
* An empty list means Automatic: built-in defaults, then the current selection.
*/
export async function resolveStructuredGenerationProviders(
options: ResolveStructuredGenerationProvidersOptions,
): Promise<StructuredGenerationProvider[]> {
@@ -53,16 +57,18 @@ export async function resolveStructuredGenerationProviders(
const entriesByProvider = new Map(enabledEntries.map((entry) => [entry.provider, entry]));
const providers: StructuredGenerationProvider[] = [];
for (const configured of configuredProviders) {
const resolvedConfigured = resolveConfiguredCandidate(
configured,
modelEntries,
entriesByProvider,
);
if (!resolvedConfigured) {
continue;
if (configuredProviders.length > 0) {
for (const configured of configuredProviders) {
const resolvedConfigured = resolveConfiguredCandidate(
configured,
modelEntries,
entriesByProvider,
);
if (resolvedConfigured) {
providers.push(resolvedConfigured);
}
}
providers.push(resolvedConfigured);
return dedupeProviders(providers);
}
for (const identifier of DEFAULT_STRUCTURED_GENERATION_PROVIDERS) {
+2
View File
@@ -973,7 +973,9 @@ export class Session {
providerSnapshotManager,
readDaemonConfig: () => this.readStructuredGenerationDaemonConfig(),
getFocusedSelection: (cwd) => this.getFocusedAgentSelectionForCwd(cwd),
logger: this.sessionLogger,
}),
logger: this.sessionLogger,
}),
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
@@ -7,6 +7,7 @@ import type { CheckoutDiffCompare, CheckoutDiffResult } from "../../../utils/che
import type { WorkspaceGitService } from "../../workspace-git-service.js";
import {
createGitMetadataGenerator,
type GitMetadataGenerator,
type StructuredTextGeneration,
type StructuredTextGenerationRequest,
} from "./git-metadata-generator.js";
@@ -38,6 +39,18 @@ function createGeneration(handler: (request: StructuredTextGenerationRequest<unk
return { generation, generateCalls };
}
function createLogger() {
const errors: Array<{ obj: object; msg?: string }> = [];
return {
errors,
logger: { error: (obj: object, msg?: string) => errors.push({ obj, msg }) },
};
}
const MANUAL_MODEL_FAILURE = new StructuredAgentFallbackError([
{ provider: "local-llm", model: "qwen-local", available: true, error: "connection refused" },
]);
const DIFF_WITH_ONE_FILE: CheckoutDiffResult = {
diff: "diff --git a/src/foo.ts b/src/foo.ts\n+added\n",
structured: [
@@ -59,7 +72,11 @@ describe("createGitMetadataGenerator", () => {
const { generation, generateCalls } = createGeneration(() => ({
message: "Fix the flaky retry test",
}));
const generator = createGitMetadataGenerator({ workspaceGitService: diffSource, generation });
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger: createLogger().logger,
});
const message = await generator.generateCommitMessage("/repo");
@@ -82,7 +99,11 @@ describe("createGitMetadataGenerator", () => {
const { generation } = createGeneration(() => {
throw new StructuredAgentFallbackError([]);
});
const generator = createGitMetadataGenerator({ workspaceGitService: diffSource, generation });
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger: createLogger().logger,
});
await expect(generator.generateCommitMessage("/repo")).resolves.toBe("Update files");
});
@@ -95,7 +116,11 @@ describe("createGitMetadataGenerator", () => {
validationErrors: ["message: required"],
});
});
const generator = createGitMetadataGenerator({ workspaceGitService: diffSource, generation });
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger: createLogger().logger,
});
await expect(generator.generateCommitMessage("/repo")).resolves.toBe("Update files");
});
@@ -105,7 +130,11 @@ describe("createGitMetadataGenerator", () => {
const { generation } = createGeneration(() => {
throw new Error("network down");
});
const generator = createGitMetadataGenerator({ workspaceGitService: diffSource, generation });
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger: createLogger().logger,
});
await expect(generator.generateCommitMessage("/repo")).rejects.toThrow("network down");
});
@@ -116,7 +145,11 @@ describe("createGitMetadataGenerator", () => {
title: "Add retry with backoff",
body: "Retries transient failures up to twice.",
}));
const generator = createGitMetadataGenerator({ workspaceGitService: diffSource, generation });
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger: createLogger().logger,
});
const result = await generator.generatePullRequestText("/repo", "main");
@@ -140,11 +173,60 @@ describe("createGitMetadataGenerator", () => {
const { generation } = createGeneration(() => {
throw new StructuredAgentFallbackError([]);
});
const generator = createGitMetadataGenerator({ workspaceGitService: diffSource, generation });
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger: createLogger().logger,
});
await expect(generator.generatePullRequestText("/repo")).resolves.toEqual({
title: "Update changes",
body: "Automated PR generated by Paseo.",
});
});
it.each([
[
"generateCommitMessage",
"CommitMessage",
(generator: GitMetadataGenerator) => generator.generateCommitMessage("/repo"),
],
[
"generatePullRequestText",
"PullRequest",
(generator: GitMetadataGenerator) => generator.generatePullRequestText("/repo", "main"),
],
] as const)("%s logs the failed provider, model and error", async (_name, schemaName, run) => {
const { diffSource } = createDiffSource(DIFF_WITH_ONE_FILE);
const { generation } = createGeneration(() => {
throw MANUAL_MODEL_FAILURE;
});
const { logger, errors } = createLogger();
const generator = createGitMetadataGenerator({
workspaceGitService: diffSource,
generation,
logger,
});
await run(generator);
expect(errors).toEqual([
{
obj: {
err: MANUAL_MODEL_FAILURE,
cwd: "/repo",
schemaName,
attempts: [
{
provider: "local-llm",
model: "qwen-local",
available: true,
error: "connection refused",
},
],
},
msg: "Structured git metadata generation failed",
},
]);
});
});
@@ -3,6 +3,7 @@ import {
StructuredAgentFallbackError,
StructuredAgentResponseError,
generateStructuredAgentResponseWithFallback,
type StructuredGenerationLogger,
} from "../../agent/agent-response-loop.js";
import type { AgentManager } from "../../agent/agent-manager.js";
import type { ProviderSnapshotManager } from "../../agent/provider-snapshot-manager.js";
@@ -37,7 +38,7 @@ export interface GitMetadataGenerator {
* behaviour is unit-testable with a fake. Production wires
* createAgentStructuredTextGeneration (resolve providers → run structured
* generation); a failed generation throws StructuredAgent*Error, which the
* generator catches and turns into the product fallback text.
* generator logs and turns into the product fallback text.
*/
export interface StructuredTextGeneration {
generate<T>(request: StructuredTextGenerationRequest<T>): Promise<T>;
@@ -51,6 +52,10 @@ export interface StructuredTextGenerationRequest<T> {
agentTitle: string;
}
interface GitMetadataLogger {
error: (obj: object, msg?: string) => void;
}
type GitMetadataDiffSource = Pick<WorkspaceGitService, "getCheckoutDiff" | "resolveRepoRoot">;
type CheckoutDiffOptions = Parameters<GitMetadataDiffSource["getCheckoutDiff"]>[1];
type CheckoutDiff = Awaited<ReturnType<GitMetadataDiffSource["getCheckoutDiff"]>>;
@@ -90,8 +95,32 @@ interface PromptForDiffInput {
export function createGitMetadataGenerator(deps: {
workspaceGitService: GitMetadataDiffSource;
generation: StructuredTextGeneration;
logger: GitMetadataLogger;
}): GitMetadataGenerator {
const { workspaceGitService, generation } = deps;
const { workspaceGitService, generation, logger } = deps;
async function generateOrFallback<T>(
request: StructuredTextGenerationRequest<T>,
fallback: T,
): Promise<T> {
try {
return await generation.generate(request);
} catch (error) {
if (!isStructuredGenerationFailure(error)) {
throw error;
}
logger.error(
{
err: error,
cwd: request.cwd,
schemaName: request.schemaName,
attempts: error instanceof StructuredAgentFallbackError ? error.attempts : undefined,
},
"Structured git metadata generation failed",
);
return fallback;
}
}
async function buildPromptForDiff(input: PromptForDiffInput): Promise<string> {
const diff = await workspaceGitService.getCheckoutDiff(input.cwd, input.diffOptions);
@@ -123,21 +152,17 @@ export function createGitMetadataGenerator(deps: {
styleDefault: "Concise, imperative mood, no trailing period.",
jsonFieldsHint: "Return JSON only with a single field 'message'.",
});
try {
const result = await generation.generate({
const result = await generateOrFallback(
{
cwd,
prompt,
schema: COMMIT_MESSAGE_SCHEMA,
schemaName: "CommitMessage",
agentTitle: "Commit generator",
});
return result.message;
} catch (error) {
if (isStructuredGenerationFailure(error)) {
return COMMIT_MESSAGE_FALLBACK;
}
throw error;
}
},
{ message: COMMIT_MESSAGE_FALLBACK },
);
return result.message;
},
async generatePullRequestText(cwd, baseRef) {
@@ -150,20 +175,16 @@ export function createGitMetadataGenerator(deps: {
styleDefault: "Clear, descriptive title; body explaining what changed and why.",
jsonFieldsHint: "Return JSON only with fields 'title' and 'body'.",
});
try {
return await generation.generate({
return generateOrFallback(
{
cwd,
prompt,
schema: PULL_REQUEST_SCHEMA,
schemaName: "PullRequest",
agentTitle: "PR generator",
});
} catch (error) {
if (isStructuredGenerationFailure(error)) {
return PULL_REQUEST_FALLBACK;
}
throw error;
}
},
PULL_REQUEST_FALLBACK,
);
},
};
}
@@ -180,6 +201,7 @@ export function createAgentStructuredTextGeneration(deps: {
getFocusedSelection: (
cwd: string,
) => ResolveStructuredGenerationProvidersOptions["currentSelection"];
logger: StructuredGenerationLogger;
}): StructuredTextGeneration {
return {
async generate({ cwd, prompt, schema, schemaName, agentTitle }) {
@@ -198,6 +220,7 @@ export function createAgentStructuredTextGeneration(deps: {
maxRetries: 2,
providers,
persistSession: false,
logger: deps.logger,
agentConfigOverrides: {
title: agentTitle,
internal: true,
@@ -234,7 +257,9 @@ function diffChangeTypeFor(file: { isNew?: boolean; isDeleted?: boolean }): "A"
return "M";
}
function isStructuredGenerationFailure(error: unknown): boolean {
function isStructuredGenerationFailure(
error: unknown,
): error is StructuredAgentResponseError | StructuredAgentFallbackError {
return (
error instanceof StructuredAgentResponseError || error instanceof StructuredAgentFallbackError
);
@@ -4,7 +4,10 @@ import path from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
import type { AgentManager } from "./agent/agent-manager.js";
import type { StructuredAgentGenerationWithFallbackOptions } from "./agent/agent-response-loop.js";
import {
StructuredAgentFallbackError,
type StructuredAgentGenerationWithFallbackOptions,
} from "./agent/agent-response-loop.js";
import {
attemptFirstAgentBranchAutoName,
type AttemptFirstAgentBranchAutoNameResult,
@@ -227,6 +230,52 @@ describe("generateBranchNameFromFirstAgentContext", () => {
]);
});
test("tries only the manual model and logs its failure", async () => {
const logger = createLogger();
const failure = new StructuredAgentFallbackError([
{ provider: "local-llm", model: "qwen-local", available: true, error: "connection refused" },
]);
const calls: StructuredAgentGenerationWithFallbackOptions<unknown>[] = [];
const result = await generateBranchNameFromFirstAgentContext({
agentManager: {} as AgentManager,
cwd: "/tmp/repo",
providerSnapshotManager: {
listProviders: vi.fn(async () => [
{
provider: "opencode",
status: "ready" as const,
enabled: true,
models: [
{ provider: "opencode", id: "minimax-m3", label: "MiniMax M3", isDefault: true },
],
},
]),
},
daemonConfig: {
metadataGeneration: { providers: [{ provider: "local-llm", model: "qwen-local" }] },
},
currentSelection: { provider: "opencode", model: "minimax-m3" },
firstAgentContext: { prompt: "Fix the login flow" },
logger,
deps: {
generateStructuredAgentResponseWithFallback: async (options) => {
calls.push(options as StructuredAgentGenerationWithFallbackOptions<unknown>);
throw failure;
},
},
});
expect(result).toBeNull();
expect(calls.map((call) => call.providers)).toEqual([
[{ provider: "local-llm", model: "qwen-local" }],
]);
expect(logger.error).toHaveBeenCalledWith(
{ err: failure, attempts: failure.attempts },
"Structured branch name generation failed",
);
});
test.each([
["paseo.json missing", undefined],
["paseo.json exists but invalid JSON", "{ nope"],
+10 -9
View File
@@ -21,29 +21,30 @@ A workspace title and its branch name are produced together from the same prompt
## How a model is chosen
You don't have to configure anything — Paseo picks a model automatically. It builds an ordered list of candidates and tries each one until a generation succeeds, so a slow or unavailable model falls through to the next.
Paseo has two modes. **Automatic** is the default and needs no configuration. **Manual** uses only the models you choose.
The candidate list is assembled in this order:
In Automatic mode, Paseo builds an ordered list of candidates and tries each one until a generation succeeds, so a slow or unavailable model falls through to the next:
1. **Providers you configured**, in the order you list them (see below).
2. **Built-in defaults**, matched against the models of the providers you have enabled:
1. **Built-in defaults**, matched against the models of the providers you have enabled:
1. a `haiku` model
2. `gpt-5.4-mini` (low reasoning)
3. `minimax-m3`
4. `nemotron-3-super`
3. **The model currently selected** for that agent or draft, as a last resort.
2. **The model currently selected** for that agent or draft, as a last resort.
Each default is a match on the model id or name, so the first enabled provider that ships a matching model wins. Anything that can't be resolved against an enabled provider is skipped. Duplicates are removed, then generation tries the list top to bottom.
Each default is a match on the model id or name, so the first enabled provider that ships a matching model wins. Defaults with no match are skipped. Duplicates are removed, then generation tries the list top to bottom. The default order prefers small, fast, cheap models for these short tasks before falling back to whatever you have selected.
The intent of the default order is to prefer small, fast, cheap models for these short tasks before falling back to whatever you have selected.
In Manual mode, Paseo tries only your configured models, in your order. It never adds the built-in defaults or the selected model, so your prompts and diffs only go to providers you chose. If every configured model fails, generation fails: workspace titles and branch names are not generated, and commits and pull requests get placeholder text. Pick a model that works for these tasks.
Every failure is written to `$PASEO_HOME/daemon.log` (`~/.paseo/daemon.log` by default) with the provider, the model and the error. Look there when a manual model doesn't produce titles, branch names, commit messages or pull request text.
## Choose a model
Open **Settings → Host → Metadata** and select **Automatic** or **Manual**. In Manual mode, Paseo tries the model you choose first, then falls back to the remaining configured models and built-in candidates. Choosing a model replaces the first entry in `agents.metadataGeneration.providers` and preserves the rest of the list.
Open **Settings → Host → Metadata** and select **Automatic** or **Manual**. In Manual mode, pick the model Paseo uses. Choosing a model replaces the first entry in `agents.metadataGeneration.providers` and preserves the rest of the list. Switching back to Automatic clears the list.
## Configure a custom fallback order
To configure more than one preferred model or control the exact order, set `agents.metadataGeneration.providers` in `~/.paseo/config.json`. Your entries are tried before the built-in defaults.
To use more than one model in Manual mode, list them in `agents.metadataGeneration.providers` in `~/.paseo/config.json`. Paseo tries your entries in order and nothing else. An empty list means Automatic.
```json
{
+10 -9
View File
@@ -19,29 +19,30 @@ Paseo 会生成以下几类元数据:
## 如何选择模型 {#how-a-model-is-chosen}
你无需进行任何配置,Paseo 会自动选择模型。它会构建一个有序的候选列表,逐个尝试,直到生成成功,因此慢或不可用的模型会让位给下一个。
Paseo 有两种模式。**自动** 是默认模式,无需任何配置。**手动** 只使用你选择的模型。
候选列表按以下顺序组成:
在自动模式下,Paseo 会构建一个有序的候选列表,逐个尝试,直到生成成功,因此慢或不可用的模型会让位给下一个:
1. **你配置的 Provider**,按你列出的顺序(见下文)。
2. **内置默认值**,与你已启用的 Provider 的模型进行匹配:
1. **内置默认值**,与你已启用的 Provider 的模型进行匹配:
1. 一个 `haiku` 模型
2. `gpt-5.4-mini`(低推理)
3. `minimax-m3`
4. `nemotron-3-super`
3. 最后的备选:该 Agent 或草稿**当前选中的模型**。
2. 最后的备选:该 Agent 或草稿**当前选中的模型**。
每个默认值按模型 id 或名称匹配,因此第一个提供匹配模型的已启用 Provider 胜出。无法在已启用 Provider 中解析的条目会被跳过。去除重复项后,生成过程从上到下依次尝试。
每个默认值按模型 id 或名称匹配,因此第一个提供匹配模型的已启用 Provider 胜出。没有匹配的默认值会被跳过。去除重复项后,生成过程从上到下依次尝试。默认顺序的意图是:对于这些简短任务,优先使用小、快、便宜的模型,最后才退回到你选中的模型。
默认顺序的意图是:对于这些简短任务,优先使用小、快、便宜的模型,最后才退回到你选中的模型。
在手动模式下,Paseo 只按你的顺序尝试你配置的模型。它不会加入内置默认值或当前选中的模型,因此你的提示词和 diff 只会发送给你选择的 Provider。如果所有配置的模型都失败,生成就会失败:不会生成工作区标题和分支名,提交和拉取请求会使用占位文本。请选择适合这些任务的模型。
每次失败都会写入 `$PASEO_HOME/daemon.log`(默认是 `~/.paseo/daemon.log`),包含 Provider、模型和错误。如果手动模型没有生成标题、分支名、提交信息或拉取请求文本,请查看这里。
## 选择模型 {#choose-a-model}
打开 **设置 → 主机 → 元数据**,选择 **自动** 或 **手动**。在手动模式下,Paseo 会先尝试你选择的模型,然后退回到其余已配置的模型和内置候选。选择模型会替换 `agents.metadataGeneration.providers` 中的第一项,并保留列表的其余部分。
打开 **设置 → 主机 → 元数据**,选择 **自动** 或 **手动**。在手动模式下,选择 Paseo 使用的模型。选择模型会替换 `agents.metadataGeneration.providers` 中的第一项,并保留列表的其余部分。切换回自动会清空该列表。
## 配置自定义的回退顺序 {#configure-a-custom-fallback-order}
如果要配置多个首选模型或控制确切的顺序,请在 `~/.paseo/config.json` 中设置 `agents.metadataGeneration.providers`。你的条目会在内置默认值之前尝试。
如果要在手动模式下使用多个模型,请在 `~/.paseo/config.json` 的 `agents.metadataGeneration.providers` 中列出它们。Paseo 按顺序尝试你的条目,不会尝试其他模型。空列表表示自动。
```json
{