feat: add all major features - Venice web UI, CLI tools, web-ui enhancements
Some checks are pending
CI / build-check-test (push) Waiting to run

CLI (coding-agent):
- web-search.ts: DuckDuckGo web search tool
- web-fetch.ts: Fetch and read web pages
- image-gen.ts: Venice AI image generation
- memory.ts: In-session memory store/recall
- browser.ts: Playwright headless browser tool
- tools/index.ts: Register all new tools
- model-resolver.ts: Venice as default provider

Web UI:
- VeniceModelBrowser.ts: Model picker with category tags
- ProvidersModelsTab.ts: Venice API key + model browser
- ProviderKeyInput.ts: Venice key validation
- ModelSelector.ts: Updated model selector
- SettingsDialog.ts: Settings wired up
- tools/index.ts: Export new tools
- utils/model-discovery.ts: Venice model fetching
- utils/format.ts: Formatting helpers
- example/main.ts: Wire up new tools in example app

jae-ai:
- env-api-keys.ts: VENICE_API_KEY mapping
- types.ts: venice in KnownProvider
- oauth/venice.ts: Venice OAuth/API key provider
- oauth/index.ts: Register Venice provider
This commit is contained in:
JAE 2026-03-25 18:32:28 +00:00
parent 29574c7c86
commit 903540fa95
30 changed files with 709 additions and 76 deletions

View file

@ -40,7 +40,7 @@ export type KnownProvider =
| "opencode"
| "opencode-go"
| "kimi-coding"
| "venice";
| "venice";
export type Provider = KnownProvider | string;
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";

View file

@ -25,10 +25,9 @@ export { antigravityOAuthProvider, loginAntigravity, refreshAntigravityToken } f
export { geminiCliOAuthProvider, loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli.js";
// OpenAI Codex (ChatGPT OAuth)
export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js";
// Venice AI (API Key)
export { loginVenice, veniceOAuthProvider, refreshVeniceToken } from "./venice.js";
export * from "./types.js";
// Venice AI (API Key)
export { loginVenice, refreshVeniceToken, veniceOAuthProvider } from "./venice.js";
// ============================================================================
// Provider Registry
@ -39,8 +38,8 @@ import { githubCopilotOAuthProvider } from "./github-copilot.js";
import { antigravityOAuthProvider } from "./google-antigravity.js";
import { geminiCliOAuthProvider } from "./google-gemini-cli.js";
import { openaiCodexOAuthProvider } from "./openai-codex.js";
import { veniceOAuthProvider } from "./venice.js";
import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js";
import { veniceOAuthProvider } from "./venice.js";
const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [
anthropicOAuthProvider,

View file

@ -15,14 +15,7 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import type {
Agent,
AgentEvent,
AgentMessage,
AgentState,
AgentTool,
ThinkingLevel,
} from "@jaeswift/jae-agent-core";
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@jaeswift/jae-agent-core";
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@jaeswift/jae-ai";
import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@jaeswift/jae-ai";
import { getDocsPath } from "../config.js";

View file

@ -6,12 +6,7 @@
* try to refresh tokens simultaneously.
*/
import {
getEnvApiKey,
type OAuthCredentials,
type OAuthLoginCallbacks,
type OAuthProviderId,
} from "@jaeswift/jae-ai";
import { getEnvApiKey, type OAuthCredentials, type OAuthLoginCallbacks, type OAuthProviderId } from "@jaeswift/jae-ai";
import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@jaeswift/jae-ai/oauth";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";

View file

@ -9,12 +9,12 @@ import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { createJiti } from "@mariozechner/jiti";
import * as _bundledPiAgentCore from "@jaeswift/jae-agent-core";
import * as _bundledPiAi from "@jaeswift/jae-ai";
import * as _bundledPiAiOauth from "@jaeswift/jae-ai/oauth";
import type { KeyId } from "@jaeswift/jae-tui";
import * as _bundledPiTui from "@jaeswift/jae-tui";
import { createJiti } from "@mariozechner/jiti";
// Static imports of packages that extensions may use.
// These MUST be static so Bun bundles them into the compiled binary.
// The virtualModules option then makes them available to extensions.

View file

@ -8,12 +8,7 @@
* - Interact with the user via UI primitives
*/
import type {
AgentMessage,
AgentToolResult,
AgentToolUpdateCallback,
ThinkingLevel,
} from "@jaeswift/jae-agent-core";
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@jaeswift/jae-agent-core";
import type {
Api,
AssistantMessageEvent,

View file

@ -0,0 +1,126 @@
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import type { AgentTool } from "@jaeswift/jae-agent-core";
import { type Static, Type } from "@sinclair/typebox";
const browserSchema = Type.Object({
action: Type.Union([
Type.Literal("navigate"),
Type.Literal("screenshot"),
Type.Literal("click"),
Type.Literal("type"),
Type.Literal("content"),
Type.Literal("close"),
], { description: "navigate: go to URL | screenshot: capture page | click: click element | type: type into element | content: get page text | close: close browser" }),
url: Type.Optional(Type.String({ description: "URL to navigate to (for navigate action)" })),
selector: Type.Optional(Type.String({ description: "CSS selector (for click/type actions)" })),
text: Type.Optional(Type.String({ description: "Text to type (for type action)" })),
wait: Type.Optional(Type.Number({ description: "Milliseconds to wait after action (default: 1000)" })),
});
export type BrowserToolInput = Static<typeof browserSchema>;
export interface BrowserToolDetails {
action: string;
url?: string;
screenshotPath?: string;
error?: string;
}
let _playwright: any = null;
let _browser: any = null;
let _page: any = null;
async function getPlaywright() {
if (!_playwright) {
try {
const { chromium } = await import("playwright");
_playwright = chromium;
} catch {
throw new Error("Playwright not installed. Run: npm install -g playwright && npx playwright install chromium");
}
}
return _playwright;
}
async function getPage() {
const pw = await getPlaywright();
if (!_browser) _browser = await pw.launch({ headless: true });
if (!_page) _page = await _browser.newPage();
return _page;
}
export const browserTool: AgentTool<typeof browserSchema, BrowserToolDetails> = {
name: "browser",
label: "Browser",
description: "Control a headless Chromium browser. Navigate pages, take screenshots, click elements, type text, and extract page content. Requires playwright.",
parameters: browserSchema,
async execute(toolCallId, params, signal) {
const { action, url, selector, text, wait = 1000 } = params;
try {
if (action === "close") {
if (_browser) { await _browser.close(); _browser = null; _page = null; }
return { content: [{ type: "text", text: "Browser closed." }], details: { action } };
}
const page = await getPage();
if (action === "navigate") {
if (!url) return { content: [{ type: "text", text: "Error: url required for navigate" }], details: { action } };
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForTimeout(wait);
const title = await page.title();
return {
content: [{ type: "text", text: `Navigated to: ${url}\nPage title: ${title}` }],
details: { action, url },
};
}
if (action === "screenshot") {
const dir = join(tmpdir(), "jae-browser");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
const path = join(dir, `screenshot-${Date.now()}.png`);
await page.screenshot({ path, fullPage: false });
const currentUrl = page.url();
return {
content: [{ type: "text", text: `Screenshot saved to: ${path}\nCurrent URL: ${currentUrl}` }],
details: { action, url: currentUrl, screenshotPath: path },
};
}
if (action === "content") {
const content = await page.evaluate(() => document.body.innerText);
const truncated = content.slice(0, 8000);
return {
content: [{ type: "text", text: truncated + (content.length > 8000 ? "\n...[truncated]" : "") }],
details: { action, url: page.url() },
};
}
if (action === "click") {
if (!selector) return { content: [{ type: "text", text: "Error: selector required for click" }], details: { action } };
await page.click(selector, { timeout: 10000 });
await page.waitForTimeout(wait);
return { content: [{ type: "text", text: `Clicked: ${selector}` }], details: { action } };
}
if (action === "type") {
if (!selector) return { content: [{ type: "text", text: "Error: selector required for type" }], details: { action } };
if (!text) return { content: [{ type: "text", text: "Error: text required for type" }], details: { action } };
await page.fill(selector, text);
await page.waitForTimeout(wait);
return { content: [{ type: "text", text: `Typed into: ${selector}` }], details: { action } };
}
return { content: [{ type: "text", text: `Unknown action: ${action}` }], details: { action } };
} catch (err: any) {
return {
content: [{ type: "text", text: `Browser error: ${err.message}` }],
details: { action, error: err.message },
};
}
},
};

View file

@ -0,0 +1,96 @@
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join } from "node:path";
import type { AgentTool } from "@jaeswift/jae-agent-core";
import { type Static, Type } from "@sinclair/typebox";
const imageGenSchema = Type.Object({
prompt: Type.String({ description: "Image generation prompt" }),
model: Type.Optional(Type.String({ description: "Venice image model (default: fluently-xl)" })),
width: Type.Optional(Type.Number({ description: "Width in pixels (default: 1024)" })),
height: Type.Optional(Type.Number({ description: "Height in pixels (default: 1024)" })),
steps: Type.Optional(Type.Number({ description: "Inference steps (default: 20)" })),
output_dir: Type.Optional(Type.String({ description: "Directory to save image (default: ./images)" })),
});
export type ImageGenToolInput = Static<typeof imageGenSchema>;
export interface ImageGenToolDetails {
path: string;
model: string;
prompt: string;
width: number;
height: number;
}
export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenToolDetails> = {
name: "generate_image",
label: "Generate Image",
description: "Generate an image using Venice AI image models and save it to disk. Uses VENICE_API_KEY or OPENAI_API_KEY env var.",
parameters: imageGenSchema,
async execute(toolCallId, params, signal) {
const {
prompt,
model = "fluently-xl",
width = 1024,
height = 1024,
steps = 20,
output_dir = "./images",
} = params;
const apiKey = process.env.VENICE_API_KEY || process.env.OPENAI_API_KEY;
if (!apiKey) {
return {
content: [{ type: "text", text: "Error: VENICE_API_KEY or OPENAI_API_KEY environment variable not set." }],
details: { path: "", model, prompt, width, height },
};
}
const body = JSON.stringify({
model,
prompt,
width,
height,
steps,
return_binary: false,
safe_mode: false,
});
const res = await fetch("https://api.venice.ai/api/v1/image/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body,
signal: signal ?? AbortSignal.timeout(60000),
});
if (!res.ok) {
const err = await res.text();
return {
content: [{ type: "text", text: `Image generation failed (${res.status}): ${err}` }],
details: { path: "", model, prompt, width, height },
};
}
const data = await res.json() as any;
const b64 = data?.images?.[0];
if (!b64) {
return {
content: [{ type: "text", text: "No image returned from Venice API." }],
details: { path: "", model, prompt, width, height },
};
}
if (!existsSync(output_dir)) mkdirSync(output_dir, { recursive: true });
const filename = `gen-${Date.now()}.png`;
const filepath = join(output_dir, filename);
writeFileSync(filepath, Buffer.from(b64, "base64"));
return {
content: [{ type: "text", text: `Image saved to: ${filepath}\nModel: ${model}\nPrompt: ${prompt}\nSize: ${width}x${height}` }],
details: { path: filepath, model, prompt, width, height },
};
},
};

View file

@ -107,7 +107,13 @@ import { createWriteTool, createWriteToolDefinition, writeTool, writeToolDefinit
export type Tool = AgentTool<any>;
export type ToolDef = ToolDefinition<any, any>;
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool];
import { webSearchTool } from "./web-search.js";
import { webFetchTool } from "./web-fetch.js";
import { imageGenTool } from "./image-gen.js";
import { memoryTool } from "./memory.js";
import { browserTool } from "./browser.js";
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool, webSearchTool, webFetchTool, imageGenTool, memoryTool, browserTool];
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];
export const allTools = {
@ -191,3 +197,10 @@ export function createAllTools(cwd: string, options?: ToolsOptions): Record<Tool
ls: createLsTool(cwd),
};
}
// ── New tools ────────────────────────────────────────────────────────────────
export { webSearchTool, type WebSearchToolInput, type WebSearchToolDetails, type WebSearchResult } from "./web-search.js";
export { webFetchTool, type WebFetchToolInput, type WebFetchToolDetails } from "./web-fetch.js";
export { imageGenTool, type ImageGenToolInput, type ImageGenToolDetails } from "./image-gen.js";
export { memoryTool, type MemoryToolInput, type MemoryToolDetails } from "./memory.js";
export { browserTool, type BrowserToolInput, type BrowserToolDetails } from "./browser.js";

View file

@ -0,0 +1,112 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { AgentTool } from "@jaeswift/jae-agent-core";
import { type Static, Type } from "@sinclair/typebox";
const MEMORY_DIR = join(homedir(), ".jae", "memory");
const MEMORY_FILE = join(MEMORY_DIR, "memories.json");
interface MemoryEntry {
id: string;
content: string;
tags: string[];
createdAt: string;
}
function loadMemories(): MemoryEntry[] {
if (!existsSync(MEMORY_FILE)) return [];
try {
return JSON.parse(readFileSync(MEMORY_FILE, "utf-8")) as MemoryEntry[];
} catch { return []; }
}
function saveMemories(entries: MemoryEntry[]): void {
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
writeFileSync(MEMORY_FILE, JSON.stringify(entries, null, 2));
}
function scoreMatch(entry: MemoryEntry, query: string): number {
const q = query.toLowerCase();
const text = (entry.content + " " + entry.tags.join(" ")).toLowerCase();
const words = q.split(/\s+/);
return words.filter(w => text.includes(w)).length / Math.max(words.length, 1);
}
const memorySchema = Type.Object({
action: Type.Union([
Type.Literal("save"),
Type.Literal("recall"),
Type.Literal("list"),
Type.Literal("delete"),
], { description: "save: store info | recall: search by query | list: show all | delete: remove by id" }),
content: Type.Optional(Type.String({ description: "Content to save (required for save action)" })),
query: Type.Optional(Type.String({ description: "Search query (required for recall action)" })),
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorisation (optional for save)" })),
id: Type.Optional(Type.String({ description: "Memory ID (required for delete action)" })),
limit: Type.Optional(Type.Number({ description: "Max results to return for recall (default: 5)" })),
});
export type MemoryToolInput = Static<typeof memorySchema>;
export interface MemoryToolDetails {
action: string;
count?: number;
id?: string;
}
export const memoryTool: AgentTool<typeof memorySchema, MemoryToolDetails> = {
name: "memory",
label: "Memory",
description: "Persistent memory across sessions. Save facts, recall by query, list all memories, or delete by ID. Stored in ~/.jae/memory/.",
parameters: memorySchema,
async execute(toolCallId, params, signal) {
const { action, content, query, tags = [], id, limit = 5 } = params;
const memories = loadMemories();
if (action === "save") {
if (!content) return { content: [{ type: "text", text: "Error: content is required for save action" }], details: { action } };
const entry: MemoryEntry = {
id: `mem-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
content,
tags,
createdAt: new Date().toISOString(),
};
memories.push(entry);
saveMemories(memories);
return { content: [{ type: "text", text: `Memory saved. ID: ${entry.id}` }], details: { action, id: entry.id } };
}
if (action === "recall") {
if (!query) return { content: [{ type: "text", text: "Error: query is required for recall action" }], details: { action } };
const scored = memories
.map(m => ({ m, score: scoreMatch(m, query) }))
.filter(x => x.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(x => x.m);
const text = scored.length === 0
? `No memories found for: ${query}`
: scored.map(m => `[${m.id}] ${m.content}${m.tags.length ? ` (tags: ${m.tags.join(", ")})` : ""}`).join("\n\n");
return { content: [{ type: "text", text }], details: { action, count: scored.length } };
}
if (action === "list") {
const text = memories.length === 0
? "No memories stored."
: memories.map(m => `[${m.id}] ${m.content.slice(0, 100)}${m.content.length > 100 ? "..." : ""}`).join("\n");
return { content: [{ type: "text", text }], details: { action, count: memories.length } };
}
if (action === "delete") {
if (!id) return { content: [{ type: "text", text: "Error: id is required for delete action" }], details: { action } };
const filtered = memories.filter(m => m.id !== id);
if (filtered.length === memories.length) return { content: [{ type: "text", text: `No memory found with ID: ${id}` }], details: { action } };
saveMemories(filtered);
return { content: [{ type: "text", text: `Memory ${id} deleted.` }], details: { action, id } };
}
return { content: [{ type: "text", text: `Unknown action: ${action}` }], details: { action } };
},
};

View file

@ -0,0 +1,67 @@
import type { AgentTool } from "@jaeswift/jae-agent-core";
import { type Static, Type } from "@sinclair/typebox";
const webFetchSchema = Type.Object({
url: Type.String({ description: "URL to fetch" }),
selector: Type.Optional(Type.String({ description: "CSS selector to extract specific content (optional)" })),
});
export type WebFetchToolInput = Static<typeof webFetchSchema>;
export interface WebFetchToolDetails {
url: string;
status: number;
contentType: string;
truncated: boolean;
}
const MAX_CHARS = 20000;
function htmlToText(html: string): string {
return html
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
.replace(/&quot;/g, '"').replace(/&nbsp;/g, " ").replace(/&#39;/g, "'")
.replace(/[ \t]+/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
export const webFetchTool: AgentTool<typeof webFetchSchema, WebFetchToolDetails> = {
name: "web_fetch",
label: "Web Fetch",
description: "Fetch and read the text content of any web page or URL. Strips HTML to plain text.",
parameters: webFetchSchema,
async execute(toolCallId, params, signal) {
const { url } = params;
try {
const res = await fetch(url, {
headers: { "User-Agent": "JAE-Agent/1.0", "Accept": "text/html,application/xhtml+xml,text/plain,*/*" },
signal: signal ?? AbortSignal.timeout(15000),
redirect: "follow",
});
const contentType = res.headers.get("content-type") || "";
let body = await res.text();
let text: string;
if (contentType.includes("html")) {
text = htmlToText(body);
} else {
text = body;
}
const truncated = text.length > MAX_CHARS;
if (truncated) text = text.slice(0, MAX_CHARS) + "\n... [truncated]";
return {
content: [{ type: "text", text: `URL: ${url}\nStatus: ${res.status}\n\n${text}` }],
details: { url, status: res.status, contentType, truncated },
};
} catch (err: any) {
return {
content: [{ type: "text", text: `Fetch failed for ${url}: ${err.message}` }],
details: { url, status: 0, contentType: "", truncated: false },
};
}
},
};

View file

@ -0,0 +1,99 @@
import type { AgentTool } from "@jaeswift/jae-agent-core";
import { Text } from "@jaeswift/jae-tui";
import { type Static, Type } from "@sinclair/typebox";
import { theme } from "../../modes/interactive/theme/theme.js";
import type { ToolRenderResultOptions } from "../extensions/types.js";
import { getTextOutput } from "./render-utils.js";
const webSearchSchema = Type.Object({
query: Type.String({ description: "Search query" }),
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5)" })),
});
export type WebSearchToolInput = Static<typeof webSearchSchema>;
export interface WebSearchResult {
title: string;
url: string;
snippet: string;
}
export interface WebSearchToolDetails {
results: WebSearchResult[];
query: string;
error?: string;
}
async function fetchDuckDuckGo(query: string, limit: number): Promise<WebSearchResult[]> {
const encoded = encodeURIComponent(query);
const url = `https://api.duckduckgo.com/?q=${encoded}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
const res = await fetch(url, {
headers: { "User-Agent": "JAE-Agent/1.0" },
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`DuckDuckGo returned ${res.status}`);
const data = await res.json() as any;
const results: WebSearchResult[] = [];
// Instant answer
if (data.AbstractText && data.AbstractURL) {
results.push({ title: data.Heading || query, url: data.AbstractURL, snippet: data.AbstractText });
}
// Related topics
for (const topic of (data.RelatedTopics || [])) {
if (results.length >= limit) break;
if (topic.FirstURL && topic.Text) {
results.push({ title: topic.Text.split(" - ")[0] || topic.Text, url: topic.FirstURL, snippet: topic.Text });
} else if (topic.Topics) {
for (const sub of topic.Topics) {
if (results.length >= limit) break;
if (sub.FirstURL && sub.Text) {
results.push({ title: sub.Text.split(" - ")[0] || sub.Text, url: sub.FirstURL, snippet: sub.Text });
}
}
}
}
// Results section
for (const r of (data.Results || [])) {
if (results.length >= limit) break;
if (r.FirstURL && r.Text) {
results.push({ title: r.Title || r.Text, url: r.FirstURL, snippet: r.Text });
}
}
return results.slice(0, limit);
}
export const webSearchTool: AgentTool<typeof webSearchSchema, WebSearchToolDetails> = {
name: "web_search",
label: "Web Search",
description: "Search the web for current information using DuckDuckGo. Returns titles, URLs, and snippets.",
parameters: webSearchSchema,
async execute(toolCallId, params, signal) {
const { query, limit = 5 } = params;
try {
const results = await fetchDuckDuckGo(query, limit);
const text = results.length === 0
? `No results found for: ${query}`
: results.map((r, i) => `[${i + 1}] ${r.title}
URL: ${r.url}
${r.snippet}`).join("
");
return {
content: [{ type: "text", text }],
details: { results, query },
};
} catch (err: any) {
const msg = `Web search failed: ${err.message}`;
return {
content: [{ type: "text", text: msg }],
details: { results: [], query, error: err.message },
};
}
},
};

View file

@ -1,7 +1,7 @@
import { Alert } from "@mariozechner/mini-lit/dist/Alert.js";
import type { Message } from "@jaeswift/jae-ai";
import type { AgentMessage, MessageRenderer } from "@jaeswift/jae-web-ui";
import { defaultConvertToLlm, registerMessageRenderer } from "@jaeswift/jae-web-ui";
import { Alert } from "@mariozechner/mini-lit/dist/Alert.js";
import { html } from "lit";
// ============================================================================

View file

@ -210,7 +210,7 @@ Feel free to use these tools when needed to provide accurate and helpful respons
// Create javascript_repl tool with access to attachments + artifacts
const replTool = createJavaScriptReplTool();
replTool.runtimeProvidersFactory = runtimeProvidersFactory;
return [replTool];
return [replTool, createWebSearchTool(), createImageGenTool(), createTTSTool()];
},
});
};

View file

@ -1,7 +1,7 @@
import type { Model } from "@jaeswift/jae-ai";
import { icon } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { Select, type SelectOption } from "@mariozechner/mini-lit/dist/Select.js";
import type { Model } from "@jaeswift/jae-ai";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";

View file

@ -1,7 +1,7 @@
import { type Context, complete, getModel } from "@jaeswift/jae-ai";
import { i18n } from "@mariozechner/mini-lit";
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { type Context, complete, getModel } from "@jaeswift/jae-ai";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { getAppStorage } from "../storage/app-storage.js";

View file

@ -0,0 +1,131 @@
import { getModels } from "@jaeswift/jae-ai";
import { html, LitElement, type TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
const TAG_COLORS: Record<string, string> = {
tools: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
vision: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
reasoning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
code: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
"image-generation": "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
"video-generation": "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
tts: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
asr: "bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-300",
embedding: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
"e2ee": "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300",
"web-search": "bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-300",
};
const CATEGORY_LABELS: Record<string, string> = {
text: "💬 Text & Chat",
image: "🖼️ Image Generation",
video: "🎬 Video Generation",
audio: "🔊 Audio (TTS / ASR)",
other: "🔧 Other",
};
function categoriseModel(tags: string[] = []): string {
if (tags.includes("image-generation") || tags.includes("inpainting")) return "image";
if (tags.includes("video-generation")) return "video";
if (tags.includes("tts") || tags.includes("asr")) return "audio";
if (tags.includes("embedding") || tags.includes("upscaling")) return "other";
return "text";
}
@customElement("venice-model-browser")
export class VeniceModelBrowser extends LitElement {
@state() private filter: string = "all";
@state() private search: string = "";
protected createRenderRoot() { return this; }
private renderTag(tag: string): TemplateResult {
const cls = TAG_COLORS[tag] ?? "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300";
return html`<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${cls}">${tag}</span>`;
}
render(): TemplateResult {
let models: any[] = [];
try {
models = (getModels("venice" as any) as any[]) || [];
} catch { models = []; }
// Group by category
const grouped: Record<string, any[]> = { text: [], image: [], video: [], audio: [], other: [] };
for (const m of models) {
const cat = categoriseModel(m.tags);
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(m);
}
const filters = [
{ id: "all", label: `All (${models.length})` },
...Object.entries(grouped)
.filter(([, ms]) => ms.length > 0)
.map(([cat, ms]) => ({ id: cat, label: `${CATEGORY_LABELS[cat] ?? cat} (${ms.length})` })),
];
const searchLower = this.search.toLowerCase();
const activeGroups = Object.entries(grouped).filter(([cat, ms]) => {
if (this.filter !== "all" && cat !== this.filter) return false;
return ms.length > 0;
});
return html`
<div class="flex flex-col gap-4">
<!-- Filter bar -->
<div class="flex flex-wrap gap-2 items-center">
${filters.map(f => html`
<button
class="px-3 py-1 rounded-full text-xs font-medium border transition-colors
${this.filter === f.id
? "bg-primary text-primary-foreground border-primary"
: "border-border text-muted-foreground hover:bg-secondary"}"
@click=${() => { this.filter = f.id; this.requestUpdate(); }}
>${f.label}</button>
`)}
<input
type="search"
placeholder="Search models..."
class="ml-auto px-3 py-1 text-sm rounded border border-border bg-background text-foreground w-48"
.value=${this.search}
@input=${(e: Event) => { this.search = (e.target as HTMLInputElement).value; this.requestUpdate(); }}
/>
</div>
<!-- Model groups -->
${activeGroups.map(([cat, ms]) => {
const filtered = searchLower ? ms.filter((m: any) => m.id.toLowerCase().includes(searchLower)) : ms;
if (!filtered.length) return html``;
return html`
<div class="flex flex-col gap-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">${CATEGORY_LABELS[cat] ?? cat}</h4>
<div class="grid grid-cols-1 gap-2">
${filtered.map((m: any) => html`
<div class="flex items-start justify-between p-3 rounded-lg border border-border bg-card hover:bg-secondary/40 transition-colors">
<div class="flex flex-col gap-1 min-w-0">
<span class="text-sm font-mono font-medium text-foreground truncate">${m.id}</span>
${m.contextWindow ? html`<span class="text-xs text-muted-foreground">${(m.contextWindow / 1000).toFixed(0)}k ctx</span>` : ""}
</div>
<div class="flex flex-wrap gap-1 justify-end ml-2 shrink-0 max-w-48">
${(m.tags ?? []).map((t: string) => this.renderTag(t))}
</div>
</div>
`)}
</div>
</div>
`;
})}
${models.length === 0 ? html`
<div class="text-sm text-muted-foreground text-center py-4">
No Venice models found. Ensure jae-ai includes Venice models.
</div>
` : ""}
</div>
`;
}
}

View file

@ -1,10 +1,10 @@
import type { Model } from "@jaeswift/jae-ai";
import { i18n } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { Input } from "@mariozechner/mini-lit/dist/Input.js";
import { Label } from "@mariozechner/mini-lit/dist/Label.js";
import { Select } from "@mariozechner/mini-lit/dist/Select.js";
import type { Model } from "@jaeswift/jae-ai";
import { html, type TemplateResult } from "lit";
import { state } from "lit/decorators.js";
import { getAppStorage } from "../storage/app-storage.js";

View file

@ -1,9 +1,9 @@
import { getModels, getProviders, type Model, modelsAreEqual } from "@jaeswift/jae-ai";
import { icon } from "@mariozechner/mini-lit";
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { getModels, getProviders, type Model, modelsAreEqual } from "@jaeswift/jae-ai";
import { html, type PropertyValues, type TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";

View file

@ -1,9 +1,10 @@
import { getProviders } from "@jaeswift/jae-ai";
import { i18n } from "@mariozechner/mini-lit";
import { Select } from "@mariozechner/mini-lit/dist/Select.js";
import { getProviders } from "@jaeswift/jae-ai";
import { html, type TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import "../components/CustomProviderCard.js";
import "../components/VeniceModelBrowser.js";
import "../components/ProviderKeyInput.js";
import { getAppStorage } from "../storage/app-storage.js";
import type {

View file

@ -1,9 +1,9 @@
import { getProviders } from "@jaeswift/jae-ai";
import { i18n } from "@mariozechner/mini-lit";
import { Dialog, DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { Input } from "@mariozechner/mini-lit/dist/Input.js";
import { Label } from "@mariozechner/mini-lit/dist/Label.js";
import { Switch } from "@mariozechner/mini-lit/dist/Switch.js";
import { getProviders } from "@jaeswift/jae-ai";
import { html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import "../components/ProviderKeyInput.js";

View file

@ -118,3 +118,5 @@ export { clearAuthToken, getAuthToken } from "./utils/auth-token.js";
export { formatCost, formatModelCost, formatTokenCount, formatUsage } from "./utils/format.js";
export { i18n, setLanguage, translations } from "./utils/i18n.js";
export { applyProxyIfNeeded, createStreamFn, isCorsError, shouldUseProxyForProvider } from "./utils/proxy-utils.js";
export { VeniceModelBrowser } from "./components/VeniceModelBrowser.js";

View file

@ -1,8 +1,8 @@
import { icon } from "@mariozechner/mini-lit";
import "@mariozechner/mini-lit/dist/MarkdownBlock.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import type { Agent, AgentMessage, AgentTool } from "@jaeswift/jae-agent-core";
import { StringEnum, type ToolCall } from "@jaeswift/jae-ai";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { type Static, Type } from "@sinclair/typebox";
import { html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";

View file

@ -44,3 +44,7 @@ export function renderTool(
}
export { getToolRenderer, registerToolRenderer };
export { webSearchTool, createWebSearchTool, type WebSearchDetails, type WebSearchResult } from "./web-search.js";
export { imageGenTool, createImageGenTool, type ImageGenDetails } from "./image-gen.js";
export { ttsTool, createTTSTool, type TTSDetails } from "./voice-tts.js";

View file

@ -1,6 +1,6 @@
import { i18n } from "@mariozechner/mini-lit";
import type { AgentTool } from "@jaeswift/jae-agent-core";
import type { ToolResultMessage } from "@jaeswift/jae-ai";
import { i18n } from "@mariozechner/mini-lit";
import { type Static, Type } from "@sinclair/typebox";
import { html } from "lit";
import { createRef, ref } from "lit/directives/ref.js";

View file

@ -1,5 +1,5 @@
import { i18n } from "@mariozechner/mini-lit";
import type { Usage } from "@jaeswift/jae-ai";
import { i18n } from "@mariozechner/mini-lit";
export function formatCost(cost: number): string {
return `$${cost.toFixed(4)}`;

View file

@ -1,5 +1,5 @@
import { LMStudioClient } from "@lmstudio/sdk";
import type { Model } from "@jaeswift/jae-ai";
import { LMStudioClient } from "@lmstudio/sdk";
import { Ollama } from "ollama/browser";
/**