Refactor speech providers to stream-first local/openai architecture

This commit is contained in:
Mohamed Boudra
2026-02-06 12:43:10 +07:00
parent d791f79efe
commit 277080a004
38 changed files with 1294 additions and 731 deletions
+4
View File
@@ -3,6 +3,7 @@ import { createAgentCommand } from './commands/agent/index.js'
import { createDaemonCommand } from './commands/daemon/index.js'
import { createPermitCommand } from './commands/permit/index.js'
import { createProviderCommand } from './commands/provider/index.js'
import { createSpeechCommand } from './commands/speech/index.js'
import { createWorktreeCommand } from './commands/worktree/index.js'
import { runLsCommand } from './commands/agent/ls.js'
import { runRunCommand } from './commands/agent/run.js'
@@ -134,6 +135,9 @@ export function createCli(): Command {
// Provider commands
program.addCommand(createProviderCommand())
// Speech model commands
program.addCommand(createSpeechCommand())
// Worktree commands
program.addCommand(createWorktreeCommand())
@@ -0,0 +1,68 @@
import type { Command } from "commander";
import type {
CommandError,
CommandOptions,
ListResult,
OutputSchema,
} from "../../output/index.js";
import { connectToDaemon } from "../../utils/client.js";
interface SpeechDownloadRow {
modelId: string;
status: "downloaded";
}
const speechDownloadSchema: OutputSchema<SpeechDownloadRow> = {
idField: "modelId",
columns: [
{ header: "MODEL", field: "modelId", width: 36 },
{ header: "STATUS", field: "status", width: 12, color: () => "green" },
],
};
export type SpeechDownloadResult = ListResult<SpeechDownloadRow>;
export interface SpeechDownloadOptions extends CommandOptions {
host?: string;
model?: string[];
}
export async function runSpeechDownloadCommand(
options: SpeechDownloadOptions,
_command: Command
): Promise<SpeechDownloadResult> {
const client = await connectToDaemon({ host: options.host });
try {
const response = await client.downloadSpeechModels({
modelIds: options.model && options.model.length > 0 ? options.model : undefined,
});
if (response.error) {
const commandError: CommandError = {
code: "SPEECH_MODELS_DOWNLOAD_FAILED",
message: response.error,
};
throw commandError;
}
return {
type: "list",
data: response.downloadedModelIds.map((modelId) => ({
modelId,
status: "downloaded" as const,
})),
schema: speechDownloadSchema,
};
} catch (error) {
if (typeof error === "object" && error && "code" in error && "message" in error) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
const commandError: CommandError = {
code: "SPEECH_MODELS_DOWNLOAD_FAILED",
message: `Failed to download speech models: ${message}`,
};
throw commandError;
} finally {
await client.close().catch(() => {});
}
}
+29
View File
@@ -0,0 +1,29 @@
import { Command } from "commander";
import { withOutput } from "../../output/index.js";
import { runSpeechModelsCommand } from "./models.js";
import { runSpeechDownloadCommand } from "./download.js";
function collectMultiple(value: string, previous: string[]): string[] {
return previous.concat([value]);
}
export function createSpeechCommand(): Command {
const speech = new Command("speech").description("Manage local speech models");
speech
.command("models")
.description("List local speech model download status")
.option("--json", "Output in JSON format")
.option("--host <host>", "Daemon host:port (default: localhost:6767)")
.action(withOutput(runSpeechModelsCommand));
speech
.command("download")
.description("Download local speech models")
.option("--model <id>", "Model ID to download (repeatable)", collectMultiple, [])
.option("--json", "Output in JSON format")
.option("--host <host>", "Daemon host:port (default: localhost:6767)")
.action(withOutput(runSpeechDownloadCommand));
return speech;
}
@@ -0,0 +1,72 @@
import type { Command } from "commander";
import type {
CommandError,
CommandOptions,
ListResult,
OutputSchema,
} from "../../output/index.js";
import { connectToDaemon } from "../../utils/client.js";
interface SpeechModelListItem {
id: string;
kind: string;
status: "downloaded" | "missing";
modelDir: string;
missingFiles: string;
}
const speechModelsSchema: OutputSchema<SpeechModelListItem> = {
idField: "id",
columns: [
{ header: "MODEL", field: "id", width: 36 },
{ header: "KIND", field: "kind", width: 12 },
{
header: "STATUS",
field: "status",
width: 12,
color: (value) => (value === "downloaded" ? "green" : "yellow"),
},
{ header: "MODEL DIR", field: "modelDir", width: 44 },
{ header: "MISSING FILES", field: "missingFiles", width: 40 },
],
};
export type SpeechModelsResult = ListResult<SpeechModelListItem>;
export interface SpeechModelsOptions extends CommandOptions {
host?: string;
}
export async function runSpeechModelsCommand(
options: SpeechModelsOptions,
_command: Command
): Promise<SpeechModelsResult> {
const client = await connectToDaemon({ host: options.host });
try {
const response = await client.listSpeechModels();
const rows: SpeechModelListItem[] = response.models
.slice()
.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id))
.map((model) => ({
id: model.id,
kind: model.kind,
status: model.isDownloaded ? "downloaded" : "missing",
modelDir: model.modelDir,
missingFiles: model.missingFiles?.join(", ") ?? "",
}));
return {
type: "list",
data: rows,
schema: speechModelsSchema,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const commandError: CommandError = {
code: "SPEECH_MODELS_LIST_FAILED",
message: `Failed to list speech models: ${message}`,
};
throw commandError;
} finally {
await client.close().catch(() => {});
}
}