fix: require user to specify the provider
This commit is contained in:
46
README.md
46
README.md
@@ -18,13 +18,17 @@ bun install # or use `mise install`
|
||||
|
||||
## Quick start
|
||||
|
||||
**1. Create a config file**
|
||||
**1. Initialize workspace**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nanobot
|
||||
bun run nanobot onboard
|
||||
```
|
||||
|
||||
`~/.config/nanobot/config.json`:
|
||||
This creates `~/.config/nanobot/` with a config file and templates.
|
||||
|
||||
**2. Edit config**
|
||||
|
||||
Add your API key and set provider/model:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -34,12 +38,13 @@ mkdir -p ~/.config/nanobot
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"model": "openrouter/anthropic/claude-sonnet-4-5"
|
||||
"provider": "openrouter",
|
||||
"model": "anthropic/claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Chat**
|
||||
**3. Chat**
|
||||
|
||||
```bash
|
||||
bun run nanobot agent
|
||||
@@ -115,7 +120,8 @@ Environment variable overrides:
|
||||
```json
|
||||
{
|
||||
"agent": {
|
||||
"model": "openrouter/anthropic/claude-sonnet-4-5",
|
||||
"provider": "openrouter",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"workspacePath": "~/.config/nanobot",
|
||||
"maxTokens": 4096,
|
||||
"contextWindowTokens": 65536,
|
||||
@@ -164,17 +170,27 @@ Environment variable overrides:
|
||||
}
|
||||
```
|
||||
|
||||
### Providers
|
||||
### Provider
|
||||
|
||||
Model names use a `provider/model` prefix scheme:
|
||||
The `agent.provider` field is **required** and must be one of:
|
||||
|
||||
| Prefix | Provider | Example |
|
||||
|--------|----------|---------|
|
||||
| `anthropic/` | Anthropic direct | `anthropic/claude-opus-4-5` |
|
||||
| `openai/` | OpenAI direct | `openai/gpt-4o` |
|
||||
| `google/` | Google direct | `google/gemini-2.5-pro` |
|
||||
| `openrouter/` | OpenRouter (any model) | `openrouter/anthropic/claude-sonnet-4-5` |
|
||||
| `ollama/` | Local Ollama | `ollama/llama3.2` |
|
||||
| Provider | Description |
|
||||
|----------|-------------|
|
||||
| `anthropic` | Anthropic direct (Claude models) |
|
||||
| `openai` | OpenAI direct (GPT models) |
|
||||
| `google` | Google direct (Gemini models) |
|
||||
| `openrouter` | OpenRouter (access to many models) |
|
||||
| `ollama` | Local Ollama instance |
|
||||
|
||||
The `agent.model` field is also **required** and should be the model ID without any provider prefix:
|
||||
|
||||
| Provider | Example Model |
|
||||
|----------|---------------|
|
||||
| `anthropic` | `claude-sonnet-4-5`, `claude-opus-4-5` |
|
||||
| `openai` | `gpt-4o`, `gpt-4o-mini` |
|
||||
| `google` | `gemini-2.5-pro`, `gemini-2.0-flash` |
|
||||
| `openrouter` | `anthropic/claude-sonnet-4-5` (OpenRouter uses its own model IDs) |
|
||||
| `ollama` | `llama3.2`, `qwen2.5` |
|
||||
|
||||
For Ollama, set `providers.ollama.apiBase` (default: `http://localhost:11434/api`).
|
||||
|
||||
|
||||
@@ -19,13 +19,14 @@ Docs directory created with 4 files (PRD.md, Architecture.md, API.md, Discoverie
|
||||
- **lint**: all `${err}` in template literals → `${String(err)}`; `String(args['key'] ?? '')` → `strArg(args, 'key')` helper; unused `onProgress` param → `_onProgress`; WebSocket `onerror` `err` type is `Event` → use `err.type`
|
||||
|
||||
## Work Queue (next steps)
|
||||
1. [x] Create workspace helper module (src/workspace.ts) with ensureWorkspace() and syncTemplates()
|
||||
1. [x] Create workspace helper module (src/cli/utils.ts) with ensureWorkspace() and syncTemplates()
|
||||
2. [x] Create onboard command (src/cli/onboard.ts) with path argument and directory-not-empty guard
|
||||
3. [x] Update src/cli/commands.ts to use ensureWorkspace() instead of inline mkdirSync
|
||||
4. [x] Typecheck and lint pass (0 errors)
|
||||
5. [x] Runtime smoke test: `bun run nanobot --help`
|
||||
6. [x] Test onboard command: `bun run nanobot onboard [path]`
|
||||
7. [ ] Test with a real Mattermost config (optional — user can do this)
|
||||
3. [x] Agent/gateway commands check workspace exists (throw if not found)
|
||||
4. [x] Added required `provider` field to agent config (values: anthropic, openai, google, openrouter, ollama)
|
||||
5. [x] Provider resolution uses explicit provider from config (no model prefix parsing)
|
||||
6. [x] Typecheck and lint pass (0 errors)
|
||||
7. [x] Test onboard and agent commands work correctly
|
||||
8. [ ] Test with a real Mattermost config (optional — user can do this)
|
||||
|
||||
## Key Decisions Made
|
||||
- Mattermost channel uses raw WebSocket + fetch (no mattermostdriver, no SSL hack)
|
||||
@@ -34,3 +35,5 @@ Docs directory created with 4 files (PRD.md, Architecture.md, API.md, Discoverie
|
||||
- Config is fresh Zod schema (no migration from Python config needed)
|
||||
- `ollama-ai-provider` package (not `@ai-sdk/ollama` which 404s on npm)
|
||||
- `strArg(args, key, fallback?)` helper exported from `agent/tools/base.ts` for safe unknown→string extraction
|
||||
- Agent config requires explicit `provider` field (no more model prefix like "anthropic/claude-...")
|
||||
- Model names are now just the raw model ID (e.g., "claude-sonnet-4-5" not "anthropic/claude-sonnet-4-5")
|
||||
|
||||
@@ -8,49 +8,20 @@
|
||||
- All dependencies installed
|
||||
- `src/` directory structure scaffolded
|
||||
- Memory bank initialized
|
||||
- All source files written (first pass):
|
||||
- `src/config/types.ts` + `src/config/loader.ts`
|
||||
- `src/bus/types.ts` + `src/bus/queue.ts`
|
||||
- `src/provider/types.ts` + `src/provider/index.ts`
|
||||
- `src/session/types.ts` + `src/session/manager.ts`
|
||||
- `src/agent/tools/base.ts` (+ `strArg` helper)
|
||||
- `src/agent/tools/filesystem.ts`
|
||||
- `src/agent/tools/shell.ts`
|
||||
- `src/agent/tools/web.ts`
|
||||
- `src/agent/tools/message.ts`
|
||||
- `src/agent/tools/spawn.ts` + `src/agent/subagent.ts`
|
||||
- `src/agent/tools/cron.ts`
|
||||
- `src/cron/types.ts` + `src/cron/service.ts`
|
||||
- `src/heartbeat/service.ts`
|
||||
- `src/agent/memory.ts`
|
||||
- `src/agent/skills.ts`
|
||||
- `src/agent/context.ts`
|
||||
- `src/agent/loop.ts`
|
||||
- `src/channels/base.ts` + `src/channels/mattermost.ts`
|
||||
- `src/channels/manager.ts`
|
||||
- `src/cli/commands.ts`
|
||||
- `index.ts`
|
||||
- All source files written (first pass)
|
||||
- Templates and skills copied from Python repo
|
||||
- **Full typecheck pass**: `tsc --noEmit` → 0 errors
|
||||
- **Full lint pass**: `oxlint` → 0 errors, 0 warnings
|
||||
- `package.json` scripts added: `start`, `dev`, `typecheck`
|
||||
- **Docs created**: `/docs/PRD.md`, `Architecture.md`, `API.md`, `Discoveries.md`
|
||||
- **Onboard command**: Created `src/cli/onboard.ts` with workspace initialization
|
||||
- **Provider config**: Added required `provider` field to agent config
|
||||
- **Workspace validation**: Agent/gateway commands throw if workspace doesn't exist
|
||||
|
||||
### 🔄 In Progress
|
||||
- Nothing
|
||||
|
||||
### ✅ Done
|
||||
- Created src/workspace.ts with ensureWorkspace(), syncTemplates(), checkWorkspaceEmpty()
|
||||
- Created src/cli/onboard.ts command with path argument
|
||||
- Updated src/cli/commands.ts to use ensureWorkspace() helper
|
||||
- Typecheck: 0 errors
|
||||
- Lint: 0 warnings
|
||||
|
||||
### 🔄 In Progress
|
||||
- Testing onboard command
|
||||
|
||||
### ⏳ Pending
|
||||
- Runtime smoke test: `bun run nanobot --help`
|
||||
- Integration test with a real Mattermost server
|
||||
|
||||
## Known Issues / Risks
|
||||
|
||||
@@ -24,6 +24,7 @@ export function agentCommand(program: Command): void {
|
||||
const model = opts.model ?? config.agent.model;
|
||||
const provider = makeProvider(
|
||||
config.providers,
|
||||
config.agent.provider,
|
||||
model,
|
||||
config.agent.maxTokens,
|
||||
config.agent.temperature,
|
||||
|
||||
@@ -24,6 +24,7 @@ export function gatewayCommand(program: Command): void {
|
||||
|
||||
const provider = makeProvider(
|
||||
config.providers,
|
||||
config.agent.provider,
|
||||
config.agent.model,
|
||||
config.agent.maxTokens,
|
||||
config.agent.temperature,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Command } from 'commander';
|
||||
import pc from 'picocolors';
|
||||
import { ConfigSchema, type Config } from '../config/types.ts';
|
||||
import { WORKSPACE_PATH } from '../config/constants.ts'
|
||||
import { ensureWorkspace, resolvePath, checkWorkspaceEmpty, syncTemplates } from './utils.ts';
|
||||
|
||||
@@ -16,12 +15,14 @@ export function onboardCommand(program: Command): void {
|
||||
.description('Initialize a new nanobot workspace with config and templates')
|
||||
.action(async (rawPath?: string) => {
|
||||
try {
|
||||
const defaultConfig: Config = ConfigSchema.parse({
|
||||
// Create a minimal config template - users must fill in provider and model
|
||||
const defaultConfig = {
|
||||
providers: {},
|
||||
agent: {
|
||||
provider: '',
|
||||
model: '',
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const targetPath = resolvePath(rawPath ?? WORKSPACE_PATH);
|
||||
const configPath = join(targetPath, 'config.json');
|
||||
|
||||
@@ -3,7 +3,6 @@ import { homedir } from 'node:os';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import pc from 'picocolors'
|
||||
import { type Config, ConfigSchema } from './types.ts';
|
||||
import { WORKSPACE_PATH } from './constants.ts'
|
||||
|
||||
const DEFAULT_CONFIG_PATH = resolve(homedir(), '.config', 'nanobot', 'config.json');
|
||||
|
||||
|
||||
@@ -40,8 +40,18 @@ export type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
|
||||
// Agent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const AgentProviderSchema = z.enum([
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'openrouter',
|
||||
'ollama',
|
||||
]);
|
||||
export type AgentProvider = z.infer<typeof AgentProviderSchema>;
|
||||
|
||||
export const AgentConfigSchema = z.object({
|
||||
model: z.string().default('anthropic/claude-sonnet-4-5'),
|
||||
provider: AgentProviderSchema,
|
||||
model: z.string(),
|
||||
workspacePath: z.string().default(WORKSPACE_PATH),
|
||||
maxTokens: z.number().int().default(4096),
|
||||
contextWindowTokens: z.number().int().default(65536),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||
import { type ModelMessage, generateText, stepCountIs } from 'ai';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
import { createOllama } from 'ollama-ai-provider';
|
||||
import type { ProvidersConfig } from '../config/types.ts';
|
||||
import type { AgentProvider, ProvidersConfig } from '../config/types.ts';
|
||||
import type { ChatOptions, LLMResponse, ToolDefinition } from './types.ts';
|
||||
|
||||
export type { ToolDefinition };
|
||||
@@ -66,17 +66,20 @@ import type { LanguageModel } from 'ai';
|
||||
|
||||
export class LLMProvider {
|
||||
private _providers: ProvidersConfig;
|
||||
private _provider: AgentProvider;
|
||||
private _defaultModel: string;
|
||||
private _maxTokens: number;
|
||||
private _temperature: number;
|
||||
|
||||
constructor(
|
||||
providers: ProvidersConfig,
|
||||
provider: AgentProvider,
|
||||
defaultModel: string,
|
||||
maxTokens = 4096,
|
||||
temperature = 0.7,
|
||||
) {
|
||||
this._providers = providers;
|
||||
this._provider = provider;
|
||||
this._defaultModel = defaultModel;
|
||||
this._maxTokens = maxTokens;
|
||||
this._temperature = temperature;
|
||||
@@ -87,39 +90,30 @@ export class LLMProvider {
|
||||
}
|
||||
|
||||
private _resolveModel(model: string): LanguageModel {
|
||||
const slashIdx = model.indexOf('/');
|
||||
const prefix = slashIdx >= 0 ? model.slice(0, slashIdx) : model;
|
||||
const remainder = slashIdx >= 0 ? model.slice(slashIdx + 1) : model;
|
||||
|
||||
switch (prefix) {
|
||||
switch (this._provider) {
|
||||
case 'anthropic': {
|
||||
const cfg = this._providers.anthropic;
|
||||
return createAnthropic({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder);
|
||||
return createAnthropic({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model);
|
||||
}
|
||||
case 'openai': {
|
||||
const cfg = this._providers.openai;
|
||||
return createOpenAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder);
|
||||
return createOpenAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model);
|
||||
}
|
||||
case 'google': {
|
||||
const cfg = this._providers.google;
|
||||
return createGoogleGenerativeAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder);
|
||||
return createGoogleGenerativeAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model);
|
||||
}
|
||||
case 'openrouter': {
|
||||
const cfg = this._providers.openrouter;
|
||||
return createOpenRouter({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(remainder);
|
||||
return createOpenRouter({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model);
|
||||
}
|
||||
case 'ollama': {
|
||||
const cfg = this._providers.ollama;
|
||||
// ollama-ai-provider returns LanguageModelV1; cast to LanguageModel (compatible at runtime)
|
||||
return createOllama({ baseURL: cfg?.apiBase ?? 'http://localhost:11434/api' })(
|
||||
remainder,
|
||||
model,
|
||||
) as unknown as LanguageModel;
|
||||
}
|
||||
default: {
|
||||
// No recognized prefix — fall through to openai-compatible
|
||||
const cfg = this._providers.openai;
|
||||
return createOpenAI({ apiKey: cfg?.apiKey, baseURL: cfg?.apiBase })(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,11 +206,12 @@ export class LLMProvider {
|
||||
|
||||
export function makeProvider(
|
||||
providers: ProvidersConfig,
|
||||
provider: AgentProvider,
|
||||
model: string,
|
||||
maxTokens: number,
|
||||
temperature: number,
|
||||
): LLMProvider {
|
||||
return new LLMProvider(providers, model, maxTokens, temperature);
|
||||
return new LLMProvider(providers, provider, model, maxTokens, temperature);
|
||||
}
|
||||
|
||||
/** Build a tool-result message to append after executing a tool call. */
|
||||
|
||||
Reference in New Issue
Block a user