Replaces the stray useClaudeCli settings checkbox + onboarding question
with a proper provider-card UX. The card lives next to OAuth + API-key
cards in onboarding and settings, with Enable/Disable + Test actions.
Backend:
- Vendors rchern/pi-claude-cli@0.3.1 as packages/pi-claude-cli
(MIT, attribution in UPSTREAM.md). Lets us bump peer-dep on
pi-coding-agent in lockstep with Fusion (upstream pinned ^0.52.0
vs ours ^0.62.0) and fix bugs without waiting on upstream.
- Adds @fusion/pi-claude-cli as a workspace dep of @runfusion/fusion
so users don't have to `npm install -g pi-claude-cli` manually.
- serve/daemon/dashboard conditionally load the extension via
discoverAndLoadExtensions() when GlobalSettings.useClaudeCli is on;
no side-effects on user ~/.fusion/agent/settings.json.
- New GET /api/providers/claude-cli/status: claude --version probe
+ toggle state + cached extension resolution.
- New POST /api/auth/claude-cli: flips useClaudeCli, refuses if the
claude binary is missing, fires the existing skill-backfill hook.
- /api/auth/status now injects a synthetic {id:"claude-cli", type:"cli"}
provider entry so onboarding + settings see a consistent list.
Frontend:
- New ClaudeCliProviderCard component shared between ModelOnboardingModal
and SettingsModal's Authentication section.
- New AuthProvider.type = "cli" variant.
- Removed the old "Route AI calls through the Claude CLI" checkbox from
Global Models settings and the opt-in step from the onboarding wizard.
- ProviderIcon gets a composite Anthropic-mark-plus-terminal glyph for
the claude-cli provider id.
Tests:
- 8 unit tests for extension resolution (@fusion/pi-claude-cli is
workspace-linked so these run in-tree).
- 2 unit tests for the binary probe.
- Existing /auth/status tests filter out the new synthetic entry so
they keep asserting structural OAuth/API-key behavior in isolation.
- The vendored package's own 296 tests still pass unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { mapThinkingEffort, isOpusModel } from "../src/thinking-config";
|
|
import type { ThinkingBudgets } from "@mariozechner/pi-ai";
|
|
|
|
describe("isOpusModel", () => {
|
|
it("returns true for claude-opus-4-6-20260301", () => {
|
|
expect(isOpusModel("claude-opus-4-6-20260301")).toBe(true);
|
|
});
|
|
|
|
it("returns false for claude-sonnet-4-5-20250929", () => {
|
|
expect(isOpusModel("claude-sonnet-4-5-20250929")).toBe(false);
|
|
});
|
|
|
|
it("returns true for future Opus models (forward-compatible)", () => {
|
|
expect(isOpusModel("claude-opus-5-20270101")).toBe(true);
|
|
});
|
|
|
|
it("returns false for non-Opus model strings", () => {
|
|
expect(isOpusModel("claude-haiku-3-5-20240307")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("mapThinkingEffort", () => {
|
|
describe("undefined reasoning", () => {
|
|
it("returns undefined when reasoning is undefined", () => {
|
|
expect(
|
|
mapThinkingEffort(undefined, "claude-sonnet-4-5", undefined),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined regardless of model", () => {
|
|
expect(
|
|
mapThinkingEffort(undefined, "claude-opus-4-6-20260301", undefined),
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("standard (non-Opus) model mapping", () => {
|
|
const model = "claude-sonnet-4-5";
|
|
|
|
it("maps minimal to low", () => {
|
|
expect(mapThinkingEffort("minimal", model, undefined)).toBe("low");
|
|
});
|
|
|
|
it("maps low to low", () => {
|
|
expect(mapThinkingEffort("low", model, undefined)).toBe("low");
|
|
});
|
|
|
|
it("maps medium to medium", () => {
|
|
expect(mapThinkingEffort("medium", model, undefined)).toBe("medium");
|
|
});
|
|
|
|
it("maps high to high", () => {
|
|
expect(mapThinkingEffort("high", model, undefined)).toBe("high");
|
|
});
|
|
|
|
it("maps xhigh to high (downgrade for non-Opus)", () => {
|
|
expect(mapThinkingEffort("xhigh", model, undefined)).toBe("high");
|
|
});
|
|
});
|
|
|
|
describe("Opus model mapping (elevated)", () => {
|
|
const model = "claude-opus-4-6-20260301";
|
|
|
|
it("maps minimal to low", () => {
|
|
expect(mapThinkingEffort("minimal", model, undefined)).toBe("low");
|
|
});
|
|
|
|
it("maps low to low", () => {
|
|
expect(mapThinkingEffort("low", model, undefined)).toBe("low");
|
|
});
|
|
|
|
it("maps medium to high (shifted up)", () => {
|
|
expect(mapThinkingEffort("medium", model, undefined)).toBe("high");
|
|
});
|
|
|
|
it("maps high to max (shifted up)", () => {
|
|
expect(mapThinkingEffort("high", model, undefined)).toBe("max");
|
|
});
|
|
|
|
it("maps xhigh to max", () => {
|
|
expect(mapThinkingEffort("xhigh", model, undefined)).toBe("max");
|
|
});
|
|
});
|
|
|
|
describe("thinkingBudgets warning", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("logs console.warn when thinkingBudgets is provided with entries", () => {
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const budgets: ThinkingBudgets = { high: 50000 };
|
|
|
|
mapThinkingEffort("high", "claude-sonnet-4-5", budgets);
|
|
|
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("thinkingBudgets are not supported"),
|
|
);
|
|
});
|
|
|
|
it("does not warn when thinkingBudgets is undefined", () => {
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
mapThinkingEffort("high", "claude-sonnet-4-5", undefined);
|
|
|
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not warn when thinkingBudgets is empty object", () => {
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
mapThinkingEffort("high", "claude-sonnet-4-5", {} as ThinkingBudgets);
|
|
|
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still returns correct effort level when budgets trigger warning", () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const budgets: ThinkingBudgets = { high: 50000 };
|
|
|
|
const result = mapThinkingEffort(
|
|
"high",
|
|
"claude-opus-4-6-20260301",
|
|
budgets,
|
|
);
|
|
expect(result).toBe("max");
|
|
});
|
|
});
|
|
|
|
describe("no modelId defaults to non-Opus behavior", () => {
|
|
it("uses standard mapping when modelId is undefined", () => {
|
|
expect(mapThinkingEffort("medium", undefined, undefined)).toBe("medium");
|
|
});
|
|
|
|
it("does not return max for xhigh when modelId is undefined", () => {
|
|
expect(mapThinkingEffort("xhigh", undefined, undefined)).toBe("high");
|
|
});
|
|
});
|
|
});
|