diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 28c676f5..d276637b 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -9,6 +9,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { + "@ai-sdk/anthropic": "^2.0.44", "@ai-sdk/google": "^2.0.25", "@ai-sdk/openai": "^2.0.53", "@modelcontextprotocol/sdk": "^1.20.2", @@ -26,6 +27,39 @@ "typescript": "^5.9.3" } }, + "node_modules/@ai-sdk/anthropic": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.44.tgz", + "integrity": "sha512-o8TfNXRzO/KZkBrcx+CL9LQsPhx7PHyqzUGjza3TJaF9WxfH1S5UQLAmEw8F7lQoHNLU0IX03WT8o8R/4JbUxQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.17" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", + "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/gateway": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.1.tgz", diff --git a/apps/cli/package.json b/apps/cli/package.json index dfd4c3bb..fbcf5b66 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -25,6 +25,7 @@ "typescript": "^5.9.3" }, "dependencies": { + "@ai-sdk/anthropic": "^2.0.44", "@ai-sdk/google": "^2.0.25", "@ai-sdk/openai": "^2.0.53", "@modelcontextprotocol/sdk": "^1.20.2", diff --git a/apps/cli/src/application/assistant/chat.ts b/apps/cli/src/application/assistant/chat.ts index 544c5c17..15969d29 100644 --- a/apps/cli/src/application/assistant/chat.ts +++ b/apps/cli/src/application/assistant/chat.ts @@ -1,5 +1,4 @@ import { streamText, ModelMessage, tool, stepCountIs } from "ai"; -import { openai } from "@ai-sdk/openai"; import * as readline from "readline/promises"; import { stdin as input, stdout as output } from "process"; import { z } from "zod"; @@ -10,8 +9,9 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { getProvider } from "../lib/models.js"; +import { DefaultModel } from "../config/config.js"; -const model = openai("gpt-4.1"); const rl = readline.createInterface({ input, output }); // Base directory for file operations - dynamically use user's home directory @@ -57,8 +57,9 @@ export async function startCopilot() { process.stdout.write("\nCopilot: "); let currentStep = 0; + const provider = getProvider(); const result = streamText({ - model: model, + model: provider(DefaultModel), messages: messages, system: `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}. diff --git a/apps/cli/src/application/config/config.ts b/apps/cli/src/application/config/config.ts index 8ee404b9..12533ce3 100644 --- a/apps/cli/src/application/config/config.ts +++ b/apps/cli/src/application/config/config.ts @@ -1,13 +1,14 @@ import path from "path"; import fs from "fs"; import { McpServerConfig } from "../entities/mcp.js"; +import { ModelConfig } from "../entities/models.js"; import { z } from "zod"; import { homedir } from "os"; // Resolve app root relative to compiled file location (dist/...) export const WorkDir = path.join(homedir(), ".rowboat"); -const baseMcpConfig = { +const baseMcpConfig: z.infer = { mcpServers: { firecrawl: { command: "npx", @@ -23,7 +24,19 @@ const baseMcpConfig = { }, }, } -} +}; + +const baseModelConfig: z.infer = { + providers: { + openai: { + flavor: "openai", + }, + }, + defaults: { + provider: "openai", + model: "gpt-4.1", + } +}; function ensureMcpConfig() { const configPath = path.join(WorkDir, "config", "mcp.json"); @@ -32,6 +45,13 @@ function ensureMcpConfig() { } } +function ensureModelConfig() { + const configPath = path.join(WorkDir, "config", "models.json"); + if (!fs.existsSync(configPath)) { + fs.writeFileSync(configPath, JSON.stringify(baseModelConfig, null, 2)); + } +} + function ensureDirs() { const ensure = (p: string) => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }; ensure(WorkDir); @@ -39,6 +59,7 @@ function ensureDirs() { ensure(path.join(WorkDir, "agents")); ensure(path.join(WorkDir, "config")); ensureMcpConfig(); + ensureModelConfig(); } ensureDirs(); @@ -50,5 +71,16 @@ function loadMcpServerConfig(): z.infer { return McpServerConfig.parse(JSON.parse(config)); } +function loadModelConfig(): z.infer { + const configPath = path.join(WorkDir, "config", "models.json"); + if (!fs.existsSync(configPath)) return baseModelConfig; + const config = fs.readFileSync(configPath, "utf8"); + return ModelConfig.parse(JSON.parse(config)); +} + const { mcpServers } = loadMcpServerConfig(); +const { providers, defaults } = loadModelConfig(); export const McpServers = mcpServers; +export const Providers = providers; +export const DefaultModel = defaults.model; +export const DefaultProvider = defaults.provider; diff --git a/apps/cli/src/application/entities/agent.ts b/apps/cli/src/application/entities/agent.ts index adea0505..e2cd52a4 100644 --- a/apps/cli/src/application/entities/agent.ts +++ b/apps/cli/src/application/entities/agent.ts @@ -27,7 +27,8 @@ export const AgentTool = z.discriminatedUnion("type", [ export const Agent = z.object({ name: z.string(), - model: z.string(), + provider: z.string().optional(), + model: z.string().optional(), description: z.string(), instructions: z.string(), tools: z.record(z.string(), AgentTool).optional(), diff --git a/apps/cli/src/application/entities/models.ts b/apps/cli/src/application/entities/models.ts new file mode 100644 index 00000000..69c7e573 --- /dev/null +++ b/apps/cli/src/application/entities/models.ts @@ -0,0 +1,15 @@ +import z from "zod"; + +export const Provider = z.object({ + flavor: z.enum(["openai", "anthropic", "google"]), + apiKey: z.string().optional(), + baseURL: z.string().optional(), +}); + +export const ModelConfig = z.object({ + providers: z.record(z.string(), Provider), + defaults: z.object({ + provider: z.string(), + model: z.string(), + }), +}); \ No newline at end of file diff --git a/apps/cli/src/application/lib/agent.ts b/apps/cli/src/application/lib/agent.ts index 4f858b93..7a1d09a9 100644 --- a/apps/cli/src/application/lib/agent.ts +++ b/apps/cli/src/application/lib/agent.ts @@ -1,14 +1,13 @@ import { Message, MessageList } from "../entities/message.js"; import { z } from "zod"; import { Step, StepInputT, StepOutputT } from "./step.js"; -import { openai } from "@ai-sdk/openai"; -import { google } from "@ai-sdk/google"; -import { generateText, ModelMessage, stepCountIs, streamText, tool, Tool, ToolSet, jsonSchema } from "ai"; +import { ModelMessage, stepCountIs, streamText, tool, Tool, ToolSet, jsonSchema } from "ai"; import { Agent, AgentTool } from "../entities/agent.js"; -import { WorkDir } from "../config/config.js"; +import { DefaultModel, WorkDir } from "../config/config.js"; import fs from "fs"; import path from "path"; import { loadWorkflow } from "./utils.js"; +import { getProvider } from "./models.js"; const BashTool = tool({ description: "Run a command in the shell", @@ -157,9 +156,9 @@ export class AgentNode implements Step { // console.log("\n\n\t>>>>\t\ttools", JSON.stringify(tools, null, 2)); + const provider = getProvider(this.agent.provider); const { fullStream } = streamText({ - model: openai("gpt-4.1"), - // model: google("gemini-2.5-flash"), + model: provider(this.agent.model || DefaultModel), messages: convertFromMessages(input), system: this.agent.instructions, stopWhen: stepCountIs(1), diff --git a/apps/cli/src/application/lib/models.ts b/apps/cli/src/application/lib/models.ts new file mode 100644 index 00000000..74a1b36d --- /dev/null +++ b/apps/cli/src/application/lib/models.ts @@ -0,0 +1,40 @@ +import { createOpenAI, OpenAIProvider } from "@ai-sdk/openai"; +import { createGoogleGenerativeAI, GoogleGenerativeAIProvider } from "@ai-sdk/google"; +import { AnthropicProvider, createAnthropic } from "@ai-sdk/anthropic"; +import { DefaultModel, DefaultProvider, Providers } from "../config/config.js"; + +const providerMap: Record = {}; + +export function getProvider(name: string = "") { + if (!name) { + name = DefaultProvider; + } + if (providerMap[name]) { + return providerMap[name]; + } + const providerConfig = Providers[name]; + if (!providerConfig) { + throw new Error(`Provider ${name} not found`); + } + switch (providerConfig.flavor) { + case "openai": + providerMap[name] = createOpenAI({ + apiKey: providerConfig.apiKey, + baseURL: providerConfig.baseURL, + }); + break; + case "anthropic": + providerMap[name] = createAnthropic({ + apiKey: providerConfig.apiKey, + baseURL: providerConfig.baseURL, + }); + break; + case "google": + providerMap[name] = createGoogleGenerativeAI({ + apiKey: providerConfig.apiKey, + baseURL: providerConfig.baseURL, + }); + break; + } + return providerMap[name]; +} \ No newline at end of file diff --git a/apps/cli/src/test.ts b/apps/cli/src/test.ts new file mode 100644 index 00000000..d88509de --- /dev/null +++ b/apps/cli/src/test.ts @@ -0,0 +1,5 @@ +import { RunEvent } from "./application/entities/workflow-event.js"; + +const obj = {"type":"tool-invocation","stepId":"test_agent","toolName":"ask-human","input":{"question":"Do you want me to run the command `date` in the terminal to show today’s date?"},"ts":"2025-11-11T06:31:20.103Z"}; + +console.log(RunEvent.parse(obj)); \ No newline at end of file