From 744c5c4a6b244d2aac38b7acc08f98a4086544ad Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 10:06:40 +0200 Subject: [PATCH 1/2] feat(vscode): discover speech-to-text models --- .changeset/dynamic-speech-to-text-models.md | 5 ++ packages/kilo-gateway/src/api/models.ts | 57 ++++++++++++++ packages/kilo-gateway/src/index.ts | 3 + packages/kilo-gateway/test/api/models.test.ts | 55 +++++++++++++- packages/kilo-vscode/src/KiloProvider.ts | 13 ++++ .../src/kilo-provider/early-message.ts | 5 ++ .../src/kilo-provider/handlers/auth.ts | 6 ++ .../kilo-vscode/src/speech-to-text/catalog.ts | 66 ++++++++++++++++ .../src/speech-to-text/transcribe.ts | 2 +- .../unit/speech-to-text-availability.test.ts | 8 +- .../tests/unit/speech-to-text-catalog.test.ts | 35 +++++++++ .../webview-ui/agent-manager/DiffPanel.tsx | 4 +- .../agent-manager/NewWorktreeDialog.tsx | 4 +- .../webview-ui/diff-viewer/DiffViewerApp.tsx | 7 +- .../diff-viewer/FullScreenDiffView.tsx | 4 +- .../src/components/chat/PromptInput.tsx | 4 +- .../src/components/settings/ModelsTab.tsx | 11 ++- .../components/speech-to-text/availability.ts | 14 +++- .../speech-to-text/model-selector.ts | 14 ++-- .../webview-ui/src/context/provider-shell.tsx | 9 ++- .../src/context/speech-to-text-models.tsx | 33 ++++++++ .../src/types/messages/extension-messages.ts | 7 ++ .../src/types/messages/webview-messages.ts | 5 ++ .../server/httpapi/groups/kilo-gateway.ts | 17 +++++ .../server/httpapi/handlers/kilo-gateway.ts | 25 +++++++ .../server/httpapi-exercise-scenarios.ts | 1 + .../kilocode/server/httpapi-public.test.ts | 9 +++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 36 +++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 32 ++++++++ packages/sdk/openapi.json | 75 +++++++++++++++++++ 30 files changed, 539 insertions(+), 27 deletions(-) create mode 100644 .changeset/dynamic-speech-to-text-models.md create mode 100644 packages/kilo-vscode/src/speech-to-text/catalog.ts create mode 100644 packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/context/speech-to-text-models.tsx diff --git a/.changeset/dynamic-speech-to-text-models.md b/.changeset/dynamic-speech-to-text-models.md new file mode 100644 index 0000000000..1d6d6225db --- /dev/null +++ b/.changeset/dynamic-speech-to-text-models.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Discover available speech-to-text models from the Kilo Gateway while retaining offline fallback support and organization model restrictions. diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index d0bb964ca5..8188c1f825 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { getKiloUrlFromToken } from "../auth/token.js" import { getDefaultHeaders, buildKiloHeaders } from "../headers.js" +import { resolveKiloGatewayBaseUrl } from "./url.js" import { KILO_API_BASE, KILO_OPENROUTER_BASE, MODELS_FETCH_TIMEOUT_MS, PROMPTS, AI_SDK_PROVIDERS } from "./constants.js" export type KiloModelsResult = { @@ -121,6 +122,16 @@ export type KiloImageModelsResult = { error?: { kind: "unauthorized" | "network" | "schema" | "http"; status?: number } } +export type KiloTranscriptionModel = { + id: string + name: string +} + +export type KiloTranscriptionModelsResult = { + models: KiloTranscriptionModel[] + error?: { kind: "unauthorized" | "network" | "schema" | "http"; status?: number } +} + /** * Fetch image-capable models from Kilo API (OpenRouter-compatible endpoint). * Uses the same raw fetch as {@link fetchKiloModels} but keeps only models @@ -145,6 +156,52 @@ export async function fetchKiloImageModels(options?: { return { models } } +export async function fetchKiloTranscriptionModels(options?: { + kilocodeToken?: string + kilocodeOrganizationId?: string + baseURL?: string +}): Promise { + const token = options?.kilocodeToken + const organizationId = options?.kilocodeOrganizationId + const url = new URL("transcription-models", resolveKiloGatewayBaseUrl({ baseURL: options?.baseURL, token })) + const response = await fetch(url, { + headers: { + ...getDefaultHeaders(), + ...buildKiloHeaders(undefined, { kilocodeOrganizationId: organizationId }), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS), + }).catch((err: unknown) => err as Error) + + if (response instanceof Error) return { models: [], error: { kind: "network" } } + if (!response.ok) { + const kind = response.status === 401 || response.status === 403 ? "unauthorized" : "http" + return { models: [], error: { kind, status: response.status } } + } + + const json = await response.json().catch(() => null) + if (!json || !Array.isArray(json.data)) return { models: [], error: { kind: "schema" } } + + const data: unknown[] = json.data + const models = data.filter(isTranscriptionModel).map((model) => ({ + id: model.id, + name: model.name, + })) + if (models.length === 0) return { models: [], error: { kind: "schema" } } + return { models } +} + +type TranscriptionModelResponse = { + id: string + name: string +} + +function isTranscriptionModel(value: unknown): value is TranscriptionModelResponse { + if (!value || typeof value !== "object") return false + const model = value as Record + return typeof model.id === "string" && typeof model.name === "string" +} + /** * Shared raw fetch + validate used by both {@link fetchKiloModels} and {@link fetchKiloImageModels}. */ diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index cee10c2f78..4df0e8a768 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -41,6 +41,9 @@ export { fetchKiloImageModels, type KiloImageModel, type KiloImageModelsResult, + fetchKiloTranscriptionModels, + type KiloTranscriptionModel, + type KiloTranscriptionModelsResult, } from "./api/models.js" export { EMPTY_KILO_EMBEDDING_MODEL_CATALOG, diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 656d27eaf0..0833ec11f3 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -1,7 +1,7 @@ // Verifies fetchKiloModels typed result and 401 fallback behaviour. import { test, expect } from "bun:test" -import { fetchKiloModels } from "../../src/api/models.js" +import { fetchKiloModels, fetchKiloTranscriptionModels } from "../../src/api/models.js" const VALID_RESPONSE = JSON.stringify({ data: [ @@ -330,3 +330,56 @@ test("keeps image-output models with tools and drops models without tools", asyn expect(result.models["test/model-a"]).toBeDefined() expect(result.models["test/no-tools"]).toBeUndefined() }) + +test("fetches and filters the transcription catalog", async () => { + const orig = globalThis.fetch + const calls: string[] = [] + stubFetch(async (input) => { + calls.push(String(input)) + return new Response( + JSON.stringify({ + data: [ + { + id: "fish-audio/transcribe-1", + name: "Fish Audio: Transcribe 1", + }, + ], + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) + + const result = await fetchKiloTranscriptionModels({ kilocodeToken: "token" }) + + ;(globalThis as any).fetch = orig + + expect(result.error).toBeUndefined() + expect(result.models).toEqual([ + { + id: "fish-audio/transcribe-1", + name: "Fish Audio: Transcribe 1", + }, + ]) + expect(calls[0]).toContain("/api/gateway/transcription-models") +}) + +test("keeps organization catalog errors from silently falling back to personal models", async () => { + const orig = globalThis.fetch + const calls: string[] = [] + const headers: Headers[] = [] + stubFetch(async (input, init) => { + calls.push(String(input)) + headers.push(new Headers(init?.headers)) + return new Response("Forbidden", { status: 403 }) + }) + + const result = await fetchKiloTranscriptionModels({ kilocodeToken: "token", kilocodeOrganizationId: "org-1" }) + + ;(globalThis as any).fetch = orig + + expect(result.models).toEqual([]) + expect(result.error?.kind).toBe("unauthorized") + expect(calls).toHaveLength(1) + expect(calls[0]).toContain("/api/gateway/transcription-models") + expect(headers[0]?.get("X-KILOCODE-ORGANIZATIONID")).toBe("org-1") +}) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 980f8e4272..bb1d8c903e 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -169,6 +169,8 @@ import type { ProjectRef, SessionRef, WorktreeRef } from "./agent-manager/projec import { indexingConsentStore, registeredProjects } from "./indexing-consent" import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway" import { fetchImageModels } from "./image-generation/models" +import { fetchSpeechToTextModels } from "./speech-to-text/catalog" +import { SPEECH_TO_TEXT_MODELS } from "./speech-to-text/models" import { stopSessionProcesses } from "./kilo-provider/background-process" import { sandboxDefault, sandboxSessionMetadata } from "./shared/sandbox-session" import { @@ -1021,6 +1023,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper exportTranscript: (sessionID) => this.handleExportSessionTranscript(sessionID), copy: (text) => vscode.env.clipboard.writeText(text), openSessions: (ids) => this.trackOpenSessions(ids), + speechToTextModels: () => this.fetchAndSendSpeechToTextModels(), }) ) { return @@ -2701,6 +2704,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage(message) } + private async fetchAndSendSpeechToTextModels(): Promise { + const result = await fetchSpeechToTextModels(this.connectionService, this.getWorkspaceDirectory()) + if (!result.ok) { + this.postMessage({ type: "speechToTextModelsLoaded" as const, models: [...SPEECH_TO_TEXT_MODELS] }) + return + } + this.postMessage({ type: "speechToTextModelsLoaded" as const, models: result.models }) + } + /** * Seed sessionStatusMap with current session statuses on connect. * Without this, the Settings panel (which has no tracked sessions) would see @@ -3944,6 +3956,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper disposeGlobal: () => this.disposeGlobal(), fetchAndSendProviders: () => this.fetchAndSendProviders(), fetchAndSendAgents: () => this.fetchAndSendAgents(), + fetchAndSendSpeechToTextModels: () => this.fetchAndSendSpeechToTextModels(), } } diff --git a/packages/kilo-vscode/src/kilo-provider/early-message.ts b/packages/kilo-vscode/src/kilo-provider/early-message.ts index f67a2144fa..abf79b49a0 100644 --- a/packages/kilo-vscode/src/kilo-provider/early-message.ts +++ b/packages/kilo-vscode/src/kilo-provider/early-message.ts @@ -17,6 +17,7 @@ type Ctx = { exportTranscript: (sessionID: string) => Promise copy: (text: string) => PromiseLike openSessions: (ids: string[]) => void + speechToTextModels: () => Promise } export async function routeEarlyMessage( @@ -64,6 +65,10 @@ export async function routeEarlyMessage( ctx.post(buildThroughputSettingMessage()) return true } + if (message.type === "requestSpeechToTextModels") { + await ctx.speechToTextModels() + return true + } if (message.type === "requestBrowserSettings") { ctx.browserSettings() return true diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts b/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts index db6e9e6d7d..07ae712b90 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts @@ -15,6 +15,7 @@ export interface AuthContext { disposeGlobal(): Promise fetchAndSendProviders(): Promise fetchAndSendAgents(): Promise + fetchAndSendSpeechToTextModels(): Promise } /** @@ -137,6 +138,11 @@ export async function handleSetOrganization(ctx: AuthContext, organizationId: st } catch (error) { console.error("[Kilo New] KiloProvider: Failed to refresh agents after org switch:", error) } + try { + await ctx.fetchAndSendSpeechToTextModels() + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to refresh speech-to-text models after org switch:", error) + } } /** Handle profile refresh request. */ diff --git a/packages/kilo-vscode/src/speech-to-text/catalog.ts b/packages/kilo-vscode/src/speech-to-text/catalog.ts new file mode 100644 index 0000000000..f9d2724dd5 --- /dev/null +++ b/packages/kilo-vscode/src/speech-to-text/catalog.ts @@ -0,0 +1,66 @@ +import type { KiloConnectionService } from "../services/cli-backend/connection-service" +import { getErrorMessage } from "../kilo-provider-utils" +import { type SpeechToTextModelDef } from "./models" + +const PATH = "/kilo/models/transcriptions" + +type CatalogModel = { + id: string + name: string +} + +export type SpeechToTextCatalogResult = { ok: true; models: SpeechToTextModelDef[] } | { ok: false; error: string } + +export async function fetchSpeechToTextModels( + connection: KiloConnectionService, + dir: string, + signal?: AbortSignal, +): Promise { + const cfg = connection.getServerConfig() + if (!cfg) return fail("Not connected to the Kilo backend") + + const auth = Buffer.from(`kilo:${cfg.password}`).toString("base64") + const url = new URL(PATH, cfg.baseUrl) + if (dir) url.searchParams.set("directory", dir) + + try { + const res = await fetch(url, { signal, headers: { Authorization: `Basic ${auth}` } }) + if (!res.ok) return fail(`Failed to fetch speech-to-text models (HTTP ${res.status})`) + + const models = parseSpeechToTextCatalog(await res.json()) + if (!models) return fail("Invalid speech-to-text model catalog") + return { ok: true, models } + } catch (err) { + return fail(getErrorMessage(err)) + } +} + +function fail(error: string): SpeechToTextCatalogResult { + return { ok: false, error } +} + +export function parseSpeechToTextCatalog(body: unknown): SpeechToTextModelDef[] | undefined { + if (!Array.isArray(body)) return undefined + const models = body.filter(isCatalogModel).map(toModel) + return models.length > 0 ? models : undefined +} + +function isCatalogModel(value: unknown): value is CatalogModel { + if (!value || typeof value !== "object") return false + const model = value as Record + return typeof model.id === "string" && typeof model.name === "string" +} + +function toModel(model: CatalogModel): SpeechToTextModelDef { + const provider = model.name.split(":", 1)[0]?.trim() || model.id.split("/", 1)[0] || "Kilo Gateway" + return { + id: model.id, + label: model.name.includes(":") ? model.name.slice(model.name.indexOf(":") + 1).trim() : model.name, + provider, + ...(isVerbatim(model.id) ? { verbatim: true } : {}), + } +} + +function isVerbatim(id: string): boolean { + return id === "openai/gpt-4o-mini-transcribe" || id === "openai/gpt-4o-transcribe" +} diff --git a/packages/kilo-vscode/src/speech-to-text/transcribe.ts b/packages/kilo-vscode/src/speech-to-text/transcribe.ts index 1e978bf028..f40f188ac8 100644 --- a/packages/kilo-vscode/src/speech-to-text/transcribe.ts +++ b/packages/kilo-vscode/src/speech-to-text/transcribe.ts @@ -54,7 +54,7 @@ export async function transcribeSpeech( "Content-Type": "application/json", }, body: JSON.stringify({ - model: model.id, + model: input.model ?? model.id, input_audio: { data: input.data, format: input.format, diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts index 8384816896..1361f8f516 100644 --- a/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts +++ b/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts @@ -23,9 +23,11 @@ describe("speech-to-text availability", () => { }) it("normalizes configured and unknown transcription models", () => { - expect(selectedSpeechToTextModel({ experimental: { speech_to_text_model: "google/chirp-3" } })).toBe( - "google/chirp-3", - ) + expect( + selectedSpeechToTextModel({ experimental: { speech_to_text_model: "google/chirp-3" } }, [ + { id: "google/chirp-3", label: "Chirp 3", provider: "Google" }, + ]), + ).toBe("google/chirp-3") expect(selectedSpeechToTextModel({ experimental: { speech_to_text_model: "unknown/model" } })).toBe( DEFAULT_SPEECH_TO_TEXT_MODEL.id, ) diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts new file mode 100644 index 0000000000..1b1f0f3cc5 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "bun:test" +import { parseSpeechToTextCatalog } from "../../src/speech-to-text/catalog" +import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../src/speech-to-text/models" + +describe("speech-to-text discovery", () => { + it("keeps transcription catalog metadata authoritative and exposes additions", () => { + const models = parseSpeechToTextCatalog([ + { + id: "fish-audio/transcribe-1", + name: "Fish Audio: Transcribe 1", + }, + { + id: "openai/gpt-4o-mini-transcribe", + name: "OpenAI: GPT-4o Mini Transcribe", + }, + ]) + + expect(models).toEqual([ + { id: "fish-audio/transcribe-1", label: "Transcribe 1", provider: "Fish Audio" }, + { + id: "openai/gpt-4o-mini-transcribe", + label: "GPT-4o Mini Transcribe", + provider: "OpenAI", + verbatim: true, + }, + ]) + expect(models?.find((model) => model.id === "openai/gpt-4o-mini-transcribe")?.verbatim).toBe(true) + }) + + it("rejects empty or malformed catalogs so callers can use the static fallback", () => { + expect(parseSpeechToTextCatalog([])).toBeUndefined() + expect(parseSpeechToTextCatalog({ data: [] })).toBeUndefined() + expect(DEFAULT_SPEECH_TO_TEXT_MODEL.id).toBe("openai/whisper-large-v3-turbo") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 083d892d55..ae15fa3cf6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -21,6 +21,7 @@ import { useProvider } from "../src/context/provider" import { useConfig } from "../src/context/config" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../src/context/speech-to-text-models" import { getDirectory, getFilename, @@ -111,8 +112,9 @@ export const DiffPanel: Component = (props) => { const provider = useProvider() const { config } = useConfig() const speech = useSpeechToText(vscode, server, { t }) + const speechModels = useSpeechToTextModels() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const sendAllKeybind = () => isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 65217564e3..3daeea8705 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -39,6 +39,7 @@ import { import { useLanguage } from "../src/context/language" import { useImageAttachments, type ImageAttachment } from "../src/hooks/useImageAttachments" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../src/context/speech-to-text-models" import { convertToMentionPath } from "../src/utils/path-mentions" import { insertSpacedText } from "../src/components/chat/prompt-input-utils" import { WandSparkles } from "@kilocode/kilo-ui/lucide" @@ -130,8 +131,9 @@ export const NewWorktreeDialog: Component<{ const sandboxRequestID = crypto.randomUUID() const sandboxVisible = () => features().sandboxControls && globalConfig().sandbox?.enabled === true const speech = useSpeechToText(vscode, server, { t }) + const speechModels = useSpeechToTextModels() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) let prior: string | null = null let request: string | undefined const cancel = () => { diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index 5ab642292e..0811537142 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -24,6 +24,7 @@ import type { DiffViewerNotice } from "../src/types/messages/extension-messages" import { DiffPickerHeader } from "./DiffPickerHeader" import { BaseBranchPicker } from "./BaseBranchPicker" import { SpeechToTextPrewarm } from "../src/components/speech-to-text/SpeechToTextPrewarm" +import { SpeechToTextModelsProvider } from "../src/context/speech-to-text-models" const NOTICE_KEYS: Record = { "snapshots-disabled": "diffViewer.notice.snapshotsDisabled", @@ -306,8 +307,10 @@ export const DiffViewerApp: Component = () => { - - + + + + diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 73ba90e60f..6f708f8bd9 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -27,6 +27,7 @@ import { useProvider } from "../src/context/provider" import { useConfig } from "../src/context/config" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../src/context/speech-to-text-models" import { FileTree } from "./FileTree" import { treeOrder } from "./file-tree-utils" import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments" @@ -113,8 +114,9 @@ export const FullScreenDiffView: Component = (props) => const provider = useProvider() const { config } = useConfig() const speech = useSpeechToText(vscode, server, { t }) + const speechModels = useSpeechToTextModels() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const sendAllKeybind = () => isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other") diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index d63b0d6a21..f649093669 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -34,6 +34,7 @@ import { hasGitChangesMention } from "../../hooks/git-changes-context-utils" import { useSlashCommand } from "../../hooks/useSlashCommand" import { useGhostText } from "../../hooks/useGhostText" import { useSpeechToText } from "../speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../../context/speech-to-text-models" import { useImageAttachments, type ImageAttachment } from "../../hooks/useImageAttachments" import { convertToMentionPath } from "../../utils/path-mentions" import { SessionMentionPicker } from "./SessionMentionPicker" @@ -341,6 +342,7 @@ export const PromptInput: Component = (props) => { const ghost = useGhostText(vscode, text, () => server.isConnected()) const speech = useSpeechToText(vscode, server, language) + const speechModels = useSpeechToTextModels() const replaceReviewComments = (next: ReviewComment[]) => { setReviewComments(next) @@ -498,7 +500,7 @@ export const PromptInput: Component = (props) => { ) const isDisabled = () => !server.isConnected() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0 const canSend = () => !isDisabled() && diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index 81f57c3968..619cf17bd1 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -7,13 +7,14 @@ import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useProvider } from "../../context/provider" import { useSession } from "../../context/session" +import { useSpeechToTextModels } from "../../context/speech-to-text-models" import { parseModelString } from "../../../../src/shared/provider-model" import { ModelSelectorBase } from "../shared/ModelSelector" import { ThinkingSelectorBase } from "../shared/ThinkingSelector" import SettingsRow from "./SettingsRow" import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../../../src/speech-to-text/models" import { hasSpeechToTextAccess, selectedSpeechToTextModel } from "../speech-to-text/availability" -import { SPEECH_TO_TEXT_MODEL_OPTIONS } from "../speech-to-text/model-selector" +import { speechToTextModelOptions } from "../speech-to-text/model-selector" import { AUTOCOMPLETE_SELECTOR_MODELS, getAutocompleteSelection } from "./autocomplete-model-selector" const ModelsTab: Component = () => { @@ -21,6 +22,7 @@ const ModelsTab: Component = () => { const language = useLanguage() const provider = useProvider() const session = useSession() + const speechModels = useSpeechToTextModels() const autocompleteProvider = () => { const v = settings()["autocomplete.provider"] @@ -42,8 +44,9 @@ const ModelsTab: Component = () => { } const subagentModel = createMemo(() => parseModelString(config().subagent_model ?? undefined)) - const speechModel = createMemo(() => selectedSpeechToTextModel(config())) - const speechOption = createMemo(() => SPEECH_TO_TEXT_MODEL_OPTIONS.find((item) => item.value === speechModel())) + const speechModel = createMemo(() => selectedSpeechToTextModel(config(), speechModels.models())) + const speechOptions = createMemo(() => speechToTextModelOptions(speechModels.models())) + const speechOption = createMemo(() => speechOptions().find((item) => item.value === speechModel())) const kiloReady = createMemo(() => hasSpeechToTextAccess(config(), provider.authStates())) const variantKey = createMemo(() => config().subagent_model ?? undefined) const subagentVariants = createMemo(() => Object.keys(provider.findModel(subagentModel())?.variants ?? {})) @@ -190,7 +193,7 @@ const ModelsTab: Component = () => { inactive={kiloReady()} >