Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,13 @@ If a piece of infra is missing from `config.json`, the server prints the exact `

## Rule of LLM Provider

**OpenRouter or local Ollama. No direct vendor SDKs.** No Anthropic / OpenAI / Gemini / Bedrock keys or SDK imports. The active backend is selected by `Config.LlmProvider` (`"openrouter"` default | `"ollama"`) and switched via `bytebell set llm-provider <openrouter|ollama>`. Ollama mode reads `Config.OllamaUrl` (default `http://localhost:11434`) and `Config.OllamaModel` (free-form — any locally-pulled model) and reports `$0` cost. All LLM calls flow through `@bb/llm`, which:
**Every backend is an entry in `@bb/llm`'s provider table.** Six ship today — `openrouter` (default), `ollama`, `anthropic`, `bedrock`, `gemini`, `openai` — selected by `Config.LlmProvider` and switched via `bytebell set llm-provider <name>`. Adding a backend means one module under `packages/llm/src/` plus one entry in `src/providers.ts`; it must never mean a new branch at a call site.

- Wraps every OpenRouter / Ollama call behind a single `askLLM` surface
- Computes per-call cost via `estimateCostUsd()` against live OpenRouter pricing for the `bytebell stats` view (short-circuits to `0` when provider is Ollama)
- **Prefer `fetch`; SDKs only where the wire protocol demands one.** Most providers speak an OpenAI-shaped `/chat/completions`, so they share `openaiCompatible.ts` — a base URL and a bearer token, not another hand-copied request builder. Two exceptions are allowed and are the only ones: `@ai-sdk/amazon-bedrock` + `ai` (Bedrock's Converse shape and SigV4 request signing, which must not be hand-rolled) and `openai` (the tool-use client, pointed at each provider's OpenAI-compatible base URL). Adding a third SDK requires raising the dependency question first.
- **All calls flow through `askLLM` / `askJsonLLM` / `askLLMWithTools`.** No package outside `@bb/llm` may import a provider module directly, and no business logic may branch on provider identity — ask the provider table for a capability instead.
- **Capability differences are data, not conditionals.** `LlmProviderEntry` carries `reportsCost` and `supportsTools`. Tool use (`askLLMWithTools`, and therefore `concept-graph`) works on every backend except Ollama, whose tool-format support varies per locally-pulled model and cannot be checked ahead of time.
- **Cost is provider-reported or zero.** Only OpenRouter returns a real figure. Never reintroduce a client-side pricing table — a hardcoded price that silently rots is worse than an honest `$0`.
- **Credentials live in `~/.bytebell/config.json`**, one key per provider (Bedrock additionally accepts SigV4 credentials or an ambient instance role), written only by `bytebell set …` or the setup wizard. The wizard renders from `packages/cli/src/llmProviders.ts`; the server's boot gate reads `requiredKeysFor(provider)`. Both are single sources of truth — do not add a second provider list.

LLM outputs are probabilistic. They must be:

Expand Down
197 changes: 143 additions & 54 deletions README.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
"workspaces": [
"packages/*"
],
"bin": {
"bytebell": "./packages/cli/src/index.ts"
},
"scripts": {
"bytebell": "bun packages/cli/src/index.ts",
"dev:server": "BYTEBELL_DEV=1 bun --watch packages/server/src/index.ts",
"dev:cli": "BYTEBELL_DEV=1 bun packages/cli/src/index.ts",
"typecheck": "tsc -b",
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/BootCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { applyInfraDefaults, checkPreflight } from "./bootConfig.ts";
import { SetupForm } from "./SetupForm.tsx";
import { error, info, success } from "./output.ts";
import { bringInfraUp, usingHonker } from "./bootInfra.ts";
import { isEmbedded } from "./infraMode.ts";
import { isCloud, isEmbedded } from "./infraMode.ts";

export function buildBootCommand(): Command {
const cmd = new Command("boot");
Expand Down Expand Up @@ -67,6 +67,16 @@ async function runBoot(): Promise<void> {
return;
}

// Cloud mode (external databases) needs no Docker either.
if (isCloud()) {
info("cloud mode — using external/cloud databases, no Docker required.");
success(`queue → redis (${getConfigValue(Config.RedisUrl)})`);
success(`doc → mongo (${getConfigValue(Config.MongoUri)})`);
success(`graph → neo4j (${getConfigValue(Config.Neo4jUri)})`);
process.stdout.write("\nNext: bytebell index <git-url> or bytebell ingest [path]\n");
return;
}

if (graphProvider === GraphProviderType.Neo4j && defaults.neo4jPassword.length === 0) {
error("internal: neo4j password is empty after applyInfraDefaults — refusing to start docker.");
process.exitCode = 1;
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,26 @@ export interface FieldProps {
mask?: boolean;
error?: string;
autoFocus?: boolean;
isFocused?: boolean;
}

export function Field({ id, label, value, onChange, mask, error, autoFocus }: FieldProps): ReactElement {
const { isFocused } = useFocus({ id, autoFocus: autoFocus === true });
export function Field({
id,
label,
value,
onChange,
mask,
error,
autoFocus,
isFocused: propFocused,
}: FieldProps): ReactElement {
const { isFocused: hookFocused } = useFocus({ id, autoFocus: autoFocus === true });
const isFocused = propFocused !== undefined ? propFocused : hookFocused;
const indicator = isFocused ? "▶" : " ";
const labelProps = isFocused ? { color: "cyan" } : {};
const masked = mask === true;
const displayValue = masked && value.length > 0 ? "•".repeat(value.length) : value;
const inputProps = masked ? { value, onChange, mask: "•" } : { value, onChange };
const inputProps = masked ? { value, onChange, mask: "•", focus: isFocused } : { value, onChange, focus: isFocused };

return (
<Box flexDirection="column">
Expand Down
Loading
Loading