mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c786e70b |
@@ -1,16 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.61 - 2026-04-23
|
||||
|
||||
### Added
|
||||
- `additionalModels` option in provider config lets you add or relabel models without replacing the full list — entries merge with runtime-discovered models (ACP) or your static `models` list. See the [Providers docs](https://paseo.sh/docs/providers).
|
||||
- New [Providers docs page](https://paseo.sh/docs/providers) covering first-class providers and every custom provider config pattern in one place.
|
||||
|
||||
### Improved
|
||||
- Pi loads your installed extensions on startup so their models show up in the model picker.
|
||||
- Resizing the explorer sidebar no longer rerenders the rest of the workspace.
|
||||
- Images in assistant messages (both file paths and inline data URLs) persist as local attachments and open in the file pane.
|
||||
|
||||
## 0.1.60 - 2026-04-22
|
||||
|
||||
### Added
|
||||
|
||||
@@ -424,48 +424,6 @@ Models and modes are discovered dynamically at runtime from the agent process. I
|
||||
|
||||
Profile models (defined in config.json) completely replace runtime-discovered models when present.
|
||||
|
||||
If you want to keep runtime-discovered models and add or relabel a few entries, use `additionalModels` instead.
|
||||
|
||||
Example: add an experimental model while keeping every model the provider discovers at runtime:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"my-agent": {
|
||||
"extends": "acp",
|
||||
"label": "My Agent",
|
||||
"command": ["my-agent", "--acp"],
|
||||
"additionalModels": [
|
||||
{ "id": "experimental-model", "label": "Experimental", "isDefault": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example: relabel a discovered model without replacing the full list:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"my-agent": {
|
||||
"extends": "acp",
|
||||
"label": "My Agent",
|
||||
"command": ["my-agent", "--acp"],
|
||||
"additionalModels": [
|
||||
{ "id": "provider/model-id", "label": "My Preferred Label" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When an `additionalModels` entry has the same `id` as a discovered model, it updates that model in place.
|
||||
|
||||
---
|
||||
|
||||
## Provider override reference
|
||||
@@ -480,7 +438,6 @@ Every entry under `agents.providers` accepts these fields:
|
||||
| `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process |
|
||||
| `env` | `Record<string, string>` | No | Environment variables to set for the agent process |
|
||||
| `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) |
|
||||
| `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) |
|
||||
| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) |
|
||||
| `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) |
|
||||
| `order` | `number` | No | Sort order in the provider list |
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ buildNpmPackage rec {
|
||||
|
||||
# To update: run `nix build` with lib.fakeHash, copy the `got:` hash.
|
||||
# CI auto-updates this when package-lock.json changes (see .github/workflows/).
|
||||
npmDepsHash = "sha256-20mGU+cFFjhyUr011d+hU0K+x/TvEXfK+HD6PQuOZ0M=";
|
||||
npmDepsHash = "sha256-p7luf7/eeQR5seXerJd42x7gY73qqOecStNZN/3wIlk=";
|
||||
|
||||
# Prevent onnxruntime-node's install script from running during automatic
|
||||
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).
|
||||
|
||||
Generated
+19
-19
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -37385,16 +37385,16 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@floating-ui/react-native": "^0.10.7",
|
||||
"@getpaseo/expo-two-way-audio": "0.1.61",
|
||||
"@getpaseo/highlight": "0.1.61",
|
||||
"@getpaseo/server": "0.1.61",
|
||||
"@getpaseo/expo-two-way-audio": "0.1.60",
|
||||
"@getpaseo/highlight": "0.1.60",
|
||||
"@getpaseo/server": "0.1.60",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
@@ -37539,11 +37539,11 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/relay": "0.1.61",
|
||||
"@getpaseo/server": "0.1.61",
|
||||
"@getpaseo/relay": "0.1.60",
|
||||
"@getpaseo/server": "0.1.60",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -37584,11 +37584,11 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "0.1.61",
|
||||
"@getpaseo/server": "0.1.61",
|
||||
"@getpaseo/cli": "0.1.60",
|
||||
"@getpaseo/server": "0.1.60",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.6.2",
|
||||
"ws": "^8.14.2"
|
||||
@@ -37622,7 +37622,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
@@ -37823,7 +37823,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/cpp": "^1.1.5",
|
||||
@@ -37849,7 +37849,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -37865,14 +37865,14 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@ai-sdk/openai": "2.0.52",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||
"@deepgram/sdk": "^3.4.0",
|
||||
"@getpaseo/highlight": "0.1.61",
|
||||
"@getpaseo/relay": "0.1.61",
|
||||
"@getpaseo/highlight": "0.1.60",
|
||||
"@getpaseo/relay": "0.1.60",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@mariozechner/pi-coding-agent": "^0.67.68",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
@@ -38627,7 +38627,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.20.3",
|
||||
"@cloudflare/workers-types": "^4.20260114.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/expo-two-way-audio",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"main": "index.ts",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "APP_VARIANT=development expo start",
|
||||
@@ -32,9 +32,9 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@floating-ui/react-native": "^0.10.7",
|
||||
"@getpaseo/expo-two-way-audio": "0.1.61",
|
||||
"@getpaseo/highlight": "0.1.61",
|
||||
"@getpaseo/server": "0.1.61",
|
||||
"@getpaseo/expo-two-way-audio": "0.1.60",
|
||||
"@getpaseo/highlight": "0.1.60",
|
||||
"@getpaseo/server": "0.1.60",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
|
||||
@@ -55,10 +55,9 @@ export function parseImageDataUrl(
|
||||
if (!parsed.mimeType.toLowerCase().startsWith("image/")) {
|
||||
return null;
|
||||
}
|
||||
const fingerprint = `${parsed.mimeType}\0${parsed.base64.length}\0${parsed.base64.slice(0, 64)}\0${parsed.base64.slice(-64)}`;
|
||||
return {
|
||||
...parsed,
|
||||
cacheKey: `data-image:${parsed.mimeType}:${parsed.base64.length}:${hashString(fingerprint)}`,
|
||||
cacheKey: `data-image:${parsed.mimeType}:${parsed.base64.length}:${hashString(parsed.base64)}`,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -81,18 +80,13 @@ export function getFileNameFromPath(path: string | null | undefined): string | n
|
||||
}
|
||||
|
||||
export function createPreviewAttachmentId(input: {
|
||||
base64: string;
|
||||
mimeType: string;
|
||||
path?: string | null;
|
||||
size?: number | null;
|
||||
modifiedAt?: string | null;
|
||||
contentLength?: number | null;
|
||||
}): string {
|
||||
const path = input.path?.trim() ?? "";
|
||||
const size = Number.isFinite(input.size) ? String(input.size) : "";
|
||||
const modifiedAt = input.modifiedAt?.trim() ?? "";
|
||||
const contentLength = Number.isFinite(input.contentLength) ? String(input.contentLength) : "";
|
||||
const hash = hashString(`${input.mimeType}\0${path}\0${size}\0${modifiedAt}\0${contentLength}`);
|
||||
return `preview_${size || contentLength || "unknown"}_${hash}`;
|
||||
const hash = hashString(`${input.mimeType}\0${path}\0${input.base64}`);
|
||||
return `preview_${input.base64.length}_${hash}`;
|
||||
}
|
||||
|
||||
export async function blobToBase64(blob: Blob): Promise<string> {
|
||||
|
||||
@@ -76,11 +76,9 @@ async function createFilePanePreview(file: ExplorerFile | null): Promise<{
|
||||
const { content: _content, ...imageFile } = file;
|
||||
const imageAttachment = await persistAttachmentFromBase64({
|
||||
id: createPreviewAttachmentId({
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType ?? "image/png",
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
modifiedAt: file.modifiedAt,
|
||||
contentLength: file.content.length,
|
||||
}),
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType,
|
||||
|
||||
@@ -635,11 +635,9 @@ function AssistantMarkdownImage({
|
||||
|
||||
return await persistAttachmentFromBase64({
|
||||
id: createPreviewAttachmentId({
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType ?? "image/png",
|
||||
path: file.path || resolution.path,
|
||||
size: file.size,
|
||||
modifiedAt: file.modifiedAt,
|
||||
contentLength: file.content.length,
|
||||
}),
|
||||
base64: file.content,
|
||||
mimeType: file.mimeType,
|
||||
@@ -657,10 +655,7 @@ function AssistantMarkdownImage({
|
||||
}
|
||||
|
||||
return await persistAttachmentFromDataUrl({
|
||||
id: createPreviewAttachmentId({
|
||||
mimeType: dataImage.mimeType,
|
||||
contentLength: dataImage.base64.length,
|
||||
}),
|
||||
id: createPreviewAttachmentId(dataImage),
|
||||
dataUrl: source,
|
||||
mimeType: dataImage.mimeType,
|
||||
});
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import type { LayoutChangeEvent } from "react-native";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useContainerWidthBelow } from "./use-container-width";
|
||||
|
||||
function layoutEvent(width: number): LayoutChangeEvent {
|
||||
return {
|
||||
nativeEvent: {
|
||||
layout: {
|
||||
width,
|
||||
height: 48,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
} as LayoutChangeEvent;
|
||||
}
|
||||
|
||||
describe("useContainerWidthBelow", () => {
|
||||
it("does not re-render for width changes that stay in the same threshold bucket", () => {
|
||||
let renderCount = 0;
|
||||
const { result } = renderHook(() => {
|
||||
renderCount += 1;
|
||||
return useContainerWidthBelow(700);
|
||||
});
|
||||
|
||||
expect(result.current.isBelow).toBe(true);
|
||||
expect(renderCount).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.onLayout(layoutEvent(650));
|
||||
result.current.onLayout(layoutEvent(620));
|
||||
result.current.onLayout(layoutEvent(699));
|
||||
});
|
||||
|
||||
expect(result.current.isBelow).toBe(true);
|
||||
expect(renderCount).toBe(1);
|
||||
});
|
||||
|
||||
it("re-renders when the width crosses the threshold", () => {
|
||||
let renderCount = 0;
|
||||
const { result } = renderHook(() => {
|
||||
renderCount += 1;
|
||||
return useContainerWidthBelow(700);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.onLayout(layoutEvent(760));
|
||||
});
|
||||
|
||||
expect(result.current.isBelow).toBe(false);
|
||||
expect(renderCount).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -16,25 +16,3 @@ export function useContainerWidth(): {
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks only whether a container is narrower than a threshold.
|
||||
*/
|
||||
export function useContainerWidthBelow(threshold: number): {
|
||||
onLayout: (e: LayoutChangeEvent) => void;
|
||||
isBelow: boolean;
|
||||
} {
|
||||
const [isBelow, setIsBelow] = useState(true);
|
||||
return {
|
||||
onLayout: useCallback(
|
||||
(e: LayoutChangeEvent) => {
|
||||
const nextIsBelow = e.nativeEvent.layout.width < threshold;
|
||||
setIsBelow((currentIsBelow) =>
|
||||
currentIsBelow === nextIsBelow ? currentIsBelow : nextIsBelow,
|
||||
);
|
||||
},
|
||||
[threshold],
|
||||
),
|
||||
isBelow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ import { findAdjacentPane } from "@/utils/split-navigation";
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||
import { isWeb, isNative } from "@/constants/platform";
|
||||
import { useContainerWidthBelow } from "@/hooks/use-container-width";
|
||||
import { useContainerWidth } from "@/hooks/use-container-width";
|
||||
|
||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000;
|
||||
@@ -1051,8 +1051,8 @@ function WorkspaceScreenContent({
|
||||
);
|
||||
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
||||
const { closingTabIds, closeTab } = useCloseTabs();
|
||||
const { onLayout: onHeaderLayout, isBelow: showCompactButtonLabels } =
|
||||
useContainerWidthBelow(700);
|
||||
const { onLayout: onHeaderLayout, width: headerWidth } = useContainerWidth();
|
||||
const showCompactButtonLabels = headerWidth < 700;
|
||||
const closeWorkspaceTabWithCleanup = useCallback(
|
||||
function closeWorkspaceTabWithCleanup(input: {
|
||||
tabId: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"type": "module",
|
||||
"files": [
|
||||
@@ -24,8 +24,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/relay": "0.1.61",
|
||||
"@getpaseo/server": "0.1.61",
|
||||
"@getpaseo/relay": "0.1.60",
|
||||
"@getpaseo/server": "0.1.60",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"main": "dist/main.js",
|
||||
@@ -13,8 +13,8 @@
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "0.1.61",
|
||||
"@getpaseo/server": "0.1.61",
|
||||
"@getpaseo/cli": "0.1.60",
|
||||
"@getpaseo/server": "0.1.60",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-updater": "^6.6.2",
|
||||
"ws": "^8.14.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"description": "Paseo backend server",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
@@ -60,8 +60,8 @@
|
||||
"@ai-sdk/openai": "2.0.52",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||
"@deepgram/sdk": "^3.4.0",
|
||||
"@getpaseo/highlight": "0.1.61",
|
||||
"@getpaseo/relay": "0.1.61",
|
||||
"@getpaseo/highlight": "0.1.60",
|
||||
"@getpaseo/relay": "0.1.60",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@mariozechner/pi-coding-agent": "^0.67.68",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
|
||||
@@ -67,7 +67,6 @@ export const ProviderOverrideSchema = z
|
||||
command: z.array(z.string().min(1)).min(1).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
models: z.array(ProviderProfileModelSchema).optional(),
|
||||
additionalModels: z.array(ProviderProfileModelSchema).optional(),
|
||||
disallowedTools: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
order: z.number().optional(),
|
||||
|
||||
@@ -666,193 +666,6 @@ describe("buildProviderRegistry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("additional models append to runtime models", async () => {
|
||||
mockState.runtimeModels.set("claude", [
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-pro",
|
||||
label: "Runtime Pro",
|
||||
},
|
||||
]);
|
||||
|
||||
const registry = buildProviderRegistry(logger, {
|
||||
providerOverrides: {
|
||||
claude: {
|
||||
additionalModels: [
|
||||
{
|
||||
id: "profile-fast",
|
||||
label: "Profile Fast",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const models = await registry.claude.fetchModels({
|
||||
cwd: "/tmp/registry-models",
|
||||
force: false,
|
||||
});
|
||||
|
||||
expect(models).toEqual([
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-pro",
|
||||
label: "Runtime Pro",
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "profile-fast",
|
||||
label: "Profile Fast",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("additional models merge onto profile replacement models", async () => {
|
||||
mockState.runtimeModels.set("claude", [
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-pro",
|
||||
label: "Runtime Pro",
|
||||
},
|
||||
]);
|
||||
|
||||
const registry = buildProviderRegistry(logger, {
|
||||
providerOverrides: {
|
||||
claude: {
|
||||
models: [
|
||||
{
|
||||
id: "profile-curated",
|
||||
label: "Profile Curated",
|
||||
},
|
||||
],
|
||||
additionalModels: [
|
||||
{
|
||||
id: "profile-extra",
|
||||
label: "Profile Extra",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const models = await registry.claude.fetchModels({
|
||||
cwd: "/tmp/registry-models",
|
||||
force: false,
|
||||
});
|
||||
|
||||
expect(models.map((model) => model.id)).toEqual(["profile-curated", "profile-extra"]);
|
||||
});
|
||||
|
||||
test("additional models override matching runtime models in place", async () => {
|
||||
mockState.runtimeModels.set("claude", [
|
||||
{
|
||||
provider: "claude",
|
||||
id: "shared-model",
|
||||
label: "Runtime Label",
|
||||
description: "Runtime description",
|
||||
metadata: {
|
||||
source: "runtime",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-only",
|
||||
label: "Runtime Only",
|
||||
},
|
||||
]);
|
||||
|
||||
const registry = buildProviderRegistry(logger, {
|
||||
providerOverrides: {
|
||||
claude: {
|
||||
additionalModels: [
|
||||
{
|
||||
id: "shared-model",
|
||||
label: "Profile Label",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const models = await registry.claude.fetchModels({
|
||||
cwd: "/tmp/registry-models",
|
||||
force: false,
|
||||
});
|
||||
|
||||
expect(models).toEqual([
|
||||
{
|
||||
provider: "claude",
|
||||
id: "shared-model",
|
||||
label: "Profile Label",
|
||||
description: "Runtime description",
|
||||
metadata: {
|
||||
source: "runtime",
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-only",
|
||||
label: "Runtime Only",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("additional model default overrides runtime default", async () => {
|
||||
mockState.runtimeModels.set("claude", [
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-default",
|
||||
label: "Runtime Default",
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-other",
|
||||
label: "Runtime Other",
|
||||
},
|
||||
]);
|
||||
|
||||
const registry = buildProviderRegistry(logger, {
|
||||
providerOverrides: {
|
||||
claude: {
|
||||
additionalModels: [
|
||||
{
|
||||
id: "profile-default",
|
||||
label: "Profile Default",
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const models = await registry.claude.fetchModels({
|
||||
cwd: "/tmp/registry-models",
|
||||
force: false,
|
||||
});
|
||||
|
||||
expect(models).toEqual([
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-default",
|
||||
label: "Runtime Default",
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "runtime-other",
|
||||
label: "Runtime Other",
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "profile-default",
|
||||
label: "Profile Default",
|
||||
isDefault: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("no profile models — runtime models returned as-is", async () => {
|
||||
mockState.runtimeModels.set("claude", [
|
||||
{
|
||||
|
||||
@@ -63,7 +63,6 @@ type ResolvedProvider = {
|
||||
definition: AgentProviderDefinition;
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
profileModels: ProviderProfileModel[];
|
||||
additionalModels: ProviderProfileModel[];
|
||||
enabled: boolean;
|
||||
createBaseClient: (logger: Logger) => AgentClient;
|
||||
};
|
||||
@@ -225,59 +224,16 @@ function mapModel(provider: AgentProvider, model: AgentModelDefinition): AgentMo
|
||||
function mergeModels(
|
||||
provider: AgentProvider,
|
||||
profileModels: ProviderProfileModel[],
|
||||
additionalModels: ProviderProfileModel[],
|
||||
runtimeModels: AgentModelDefinition[],
|
||||
): AgentModelDefinition[] {
|
||||
const baseModels =
|
||||
profileModels.length === 0
|
||||
? runtimeModels.map((model) => mapModel(provider, model))
|
||||
: profileModels.map((model) => ({
|
||||
...model,
|
||||
provider,
|
||||
}));
|
||||
|
||||
if (additionalModels.length === 0) {
|
||||
return baseModels;
|
||||
if (profileModels.length === 0) {
|
||||
return runtimeModels.map((model) => mapModel(provider, model));
|
||||
}
|
||||
|
||||
const mergedModels = [...baseModels];
|
||||
let hasAdditionalDefault = false;
|
||||
|
||||
for (const model of additionalModels) {
|
||||
const additionalModel = {
|
||||
...model,
|
||||
provider,
|
||||
};
|
||||
hasAdditionalDefault ||= additionalModel.isDefault === true;
|
||||
|
||||
const existingIndex = mergedModels.findIndex((candidate) => candidate.id === model.id);
|
||||
if (existingIndex === -1) {
|
||||
mergedModels.push(additionalModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
mergedModels[existingIndex] = {
|
||||
...mergedModels[existingIndex],
|
||||
...additionalModel,
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasAdditionalDefault) {
|
||||
return mergedModels;
|
||||
}
|
||||
|
||||
const additionalDefaultIds = new Set(
|
||||
additionalModels.filter((model) => model.isDefault === true).map((model) => model.id),
|
||||
);
|
||||
|
||||
return mergedModels.map((model) =>
|
||||
additionalDefaultIds.has(model.id)
|
||||
? model
|
||||
: {
|
||||
...model,
|
||||
isDefault: false,
|
||||
},
|
||||
);
|
||||
return profileModels.map((model) => ({
|
||||
...model,
|
||||
provider,
|
||||
}));
|
||||
}
|
||||
|
||||
function wrapSessionProvider(provider: AgentProvider, inner: AgentSession): AgentSession {
|
||||
@@ -374,12 +330,7 @@ function createRegistryEntry(
|
||||
return inner.provider === provider ? inner : wrapClientProvider(provider, inner);
|
||||
},
|
||||
fetchModels: async (options: ListModelsOptions) =>
|
||||
mergeModels(
|
||||
provider,
|
||||
resolved.profileModels,
|
||||
resolved.additionalModels,
|
||||
await modelClient.listModels(options),
|
||||
),
|
||||
mergeModels(provider, resolved.profileModels, await modelClient.listModels(options)),
|
||||
fetchModes: async (options: ListModesOptions) => {
|
||||
const modes = modelClient.listModes
|
||||
? await modelClient.listModes(options)
|
||||
@@ -422,7 +373,6 @@ function buildResolvedBuiltinProviders(
|
||||
definition: applyOverrideToDefinition(definition, override),
|
||||
runtimeSettings: mergedRuntimeSettings,
|
||||
profileModels: override?.models ?? [],
|
||||
additionalModels: override?.additionalModels ?? [],
|
||||
enabled: override?.enabled !== false,
|
||||
createBaseClient: (logger) =>
|
||||
factory(logger, mergedRuntimeSettings, {
|
||||
@@ -466,7 +416,6 @@ function addDerivedProviders(
|
||||
),
|
||||
runtimeSettings: toRuntimeSettings(override),
|
||||
profileModels: override.models ?? [],
|
||||
additionalModels: override.additionalModels ?? [],
|
||||
enabled: override.enabled !== false,
|
||||
createBaseClient: (logger) =>
|
||||
new GenericACPAgentClient({
|
||||
@@ -496,7 +445,6 @@ function addDerivedProviders(
|
||||
definition: createDerivedDefinition(providerId, baseDefinition, override),
|
||||
runtimeSettings: mergedRuntimeSettings,
|
||||
profileModels: override.models ?? [],
|
||||
additionalModels: override.additionalModels ?? [],
|
||||
enabled: override.enabled !== false,
|
||||
createBaseClient: (logger) => baseFactory(logger, mergedRuntimeSettings),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import pino from "pino";
|
||||
@@ -96,126 +93,4 @@ describe("PiDirectAgentClient", () => {
|
||||
expect(registry.getAll).not.toHaveBeenCalled();
|
||||
expect(models.map((model) => model.id)).toEqual(["anthropic/claude-opus-4-5"]);
|
||||
});
|
||||
|
||||
test("loads project extensions before listing available models", async () => {
|
||||
const testRoot = await mkdtemp(join(tmpdir(), "paseo-pi-extension-"));
|
||||
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
|
||||
try {
|
||||
const agentDir = join(testRoot, "agent");
|
||||
const cwd = join(testRoot, "project");
|
||||
const extensionDir = join(cwd, ".pi", "extensions");
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
|
||||
await mkdir(extensionDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(extensionDir, "dummy-provider.ts"),
|
||||
`
|
||||
export default function(pi) {
|
||||
pi.registerProvider("paseo-dummy", {
|
||||
baseUrl: "https://example.invalid/v1",
|
||||
apiKey: "paseo-test-key",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "extension-model",
|
||||
name: "Extension Model",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const client = new PiDirectAgentClient({
|
||||
logger: pino({ level: "silent" }),
|
||||
});
|
||||
|
||||
const models = await client.listModels({ cwd, force: false });
|
||||
|
||||
expect(models.map((model) => model.id)).toContain("paseo-dummy/extension-model");
|
||||
} finally {
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
await rm(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("creates sessions with project extension models and exposes extension commands", async () => {
|
||||
const testRoot = await mkdtemp(join(tmpdir(), "paseo-pi-extension-session-"));
|
||||
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
|
||||
try {
|
||||
const agentDir = join(testRoot, "agent");
|
||||
const cwd = join(testRoot, "project");
|
||||
const extensionDir = join(cwd, ".pi", "extensions");
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
|
||||
await mkdir(extensionDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(extensionDir, "dummy-command.ts"),
|
||||
`
|
||||
export default function(pi) {
|
||||
pi.registerProvider("paseo-dummy", {
|
||||
baseUrl: "https://example.invalid/v1",
|
||||
apiKey: "paseo-test-key",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "extension-model",
|
||||
name: "Extension Model",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
pi.registerCommand("dummy-command", {
|
||||
description: "Dummy extension command",
|
||||
handler: async () => {}
|
||||
});
|
||||
}
|
||||
`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const client = new PiDirectAgentClient({
|
||||
logger: pino({ level: "silent" }),
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "pi",
|
||||
cwd,
|
||||
model: "paseo-dummy/extension-model",
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(session.listCommands()).resolves.toContainEqual({
|
||||
name: "dummy-command",
|
||||
description: "Dummy extension command",
|
||||
argumentHint: "",
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
} finally {
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
await rm(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,12 @@ import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import type { Logger } from "pino";
|
||||
import {
|
||||
AuthStorage,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
createAgentSession,
|
||||
type AgentSession as PiAgentSession,
|
||||
type AgentSessionEvent,
|
||||
type AgentSessionServices,
|
||||
type BashToolInput,
|
||||
type EditToolInput,
|
||||
type FindToolInput,
|
||||
@@ -102,11 +101,6 @@ interface StartTurnResult {
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
interface PiDirectSessionResult {
|
||||
session: PiAgentSession;
|
||||
modelRegistry: ModelRegistry;
|
||||
}
|
||||
|
||||
export type PiDirectSessionAdapter = Pick<
|
||||
PiAgentSession,
|
||||
| "abort"
|
||||
@@ -1365,48 +1359,48 @@ export class PiDirectAgentClient implements AgentClient {
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
}
|
||||
|
||||
private async getSessionServices(cwd: string): Promise<AgentSessionServices> {
|
||||
return createAgentSessionServices({
|
||||
cwd,
|
||||
...(this.modelRegistry ? { modelRegistry: this.modelRegistry } : {}),
|
||||
});
|
||||
private getModelRegistry(): ModelRegistry {
|
||||
if (!this.modelRegistry) {
|
||||
this.modelRegistry = ModelRegistry.create(AuthStorage.create());
|
||||
}
|
||||
return this.modelRegistry;
|
||||
}
|
||||
|
||||
private resolveConfiguredModel(
|
||||
registry: PiDirectModelRegistry,
|
||||
modelId: string | null | undefined,
|
||||
): Model<Api> | undefined {
|
||||
private resolveConfiguredModel(modelId: string | null | undefined): Model<Api> | undefined {
|
||||
const parsedReference = parseModelReference(modelId ?? null);
|
||||
if (!parsedReference) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const registry = this.getModelRegistry();
|
||||
return findModelInRegistry(registry, parsedReference);
|
||||
}
|
||||
|
||||
private async createSdkSession(config: AgentSessionConfig): Promise<PiDirectSessionResult> {
|
||||
private async createSdkSession(config: AgentSessionConfig): Promise<PiAgentSession> {
|
||||
const thinkingLevel =
|
||||
normalizePiThinkingOption(config.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL;
|
||||
const services = await this.getSessionServices(config.cwd);
|
||||
const model = this.resolveConfiguredModel(services.modelRegistry, config.model);
|
||||
const modelRegistry = this.getModelRegistry();
|
||||
const model = this.resolveConfiguredModel(config.model);
|
||||
|
||||
const { session } = await createAgentSessionFromServices({
|
||||
services,
|
||||
const sessionOptions = {
|
||||
cwd: config.cwd,
|
||||
modelRegistry,
|
||||
sessionManager: SessionManager.create(config.cwd),
|
||||
thinkingLevel,
|
||||
...(model ? { model } : {}),
|
||||
});
|
||||
};
|
||||
const { session } = await createAgentSession(sessionOptions);
|
||||
await session.bindExtensions({});
|
||||
applySystemPrompt(session, config.systemPrompt);
|
||||
return { session, modelRegistry: services.modelRegistry };
|
||||
return session;
|
||||
}
|
||||
|
||||
async createSession(
|
||||
config: AgentSessionConfig,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
const { session, modelRegistry } = await this.createSdkSession(config);
|
||||
return new PiDirectAgentSession(session, modelRegistry, config);
|
||||
const session = await this.createSdkSession(config);
|
||||
return new PiDirectAgentSession(session, this.getModelRegistry(), config);
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
@@ -1440,34 +1434,37 @@ export class PiDirectAgentClient implements AgentClient {
|
||||
modeId: overrides?.modeId,
|
||||
};
|
||||
|
||||
const services = await this.getSessionServices(mergedConfig.cwd);
|
||||
const model = this.resolveConfiguredModel(services.modelRegistry, mergedConfig.model);
|
||||
const model = this.resolveConfiguredModel(mergedConfig.model);
|
||||
const thinkingLevel = normalizePiThinkingOption(mergedConfig.thinkingOptionId);
|
||||
const { session } = await createAgentSessionFromServices({
|
||||
services,
|
||||
const sessionOptions = {
|
||||
cwd: mergedConfig.cwd,
|
||||
modelRegistry: this.getModelRegistry(),
|
||||
sessionManager: resumedManager,
|
||||
...(model ? { model } : {}),
|
||||
...(thinkingLevel ? { thinkingLevel } : {}),
|
||||
});
|
||||
};
|
||||
const { session } = await createAgentSession(sessionOptions);
|
||||
await session.bindExtensions({});
|
||||
applySystemPrompt(session, mergedConfig.systemPrompt);
|
||||
return new PiDirectAgentSession(session, services.modelRegistry, mergedConfig);
|
||||
return new PiDirectAgentSession(session, this.getModelRegistry(), mergedConfig);
|
||||
}
|
||||
|
||||
async listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
const services = await this.getSessionServices(options.cwd);
|
||||
const models = services.modelRegistry.getAvailable().map((model) => ({
|
||||
provider: PI_PROVIDER,
|
||||
id: `${model.provider}/${model.id}`,
|
||||
label: `${model.provider}/${model.name}`,
|
||||
description: `${model.provider}/${model.id}`,
|
||||
metadata: {
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
} satisfies AgentMetadata,
|
||||
thinkingOptions: model.reasoning ? PI_THINKING_OPTIONS.map(mapThinkingOption) : undefined,
|
||||
defaultThinkingOptionId: model.reasoning ? DEFAULT_PI_THINKING_LEVEL : undefined,
|
||||
}));
|
||||
async listModels(_options: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
// Pi Direct uses an in-process global registry; cwd/force are intentionally irrelevant.
|
||||
const models = this.getModelRegistry()
|
||||
.getAvailable()
|
||||
.map((model) => ({
|
||||
provider: PI_PROVIDER,
|
||||
id: `${model.provider}/${model.id}`,
|
||||
label: `${model.provider}/${model.name}`,
|
||||
description: `${model.provider}/${model.id}`,
|
||||
metadata: {
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
} satisfies AgentMetadata,
|
||||
thinkingOptions: model.reasoning ? PI_THINKING_OPTIONS.map(mapThinkingOption) : undefined,
|
||||
defaultThinkingOptionId: model.reasoning ? DEFAULT_PI_THINKING_LEVEL : undefined,
|
||||
}));
|
||||
|
||||
return transformPiModels(models);
|
||||
}
|
||||
|
||||
@@ -165,34 +165,6 @@ describe("provider overrides (new format)", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("additionalModels array accepted", () => {
|
||||
const parsed = PersistedConfigSchema.parse({
|
||||
agents: {
|
||||
providers: {
|
||||
zai: {
|
||||
extends: "claude",
|
||||
label: "ZAI",
|
||||
additionalModels: [
|
||||
{
|
||||
id: "zai-fast",
|
||||
label: "ZAI Fast",
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.agents?.providers?.zai?.additionalModels).toEqual([
|
||||
{
|
||||
id: "zai-fast",
|
||||
label: "ZAI Fast",
|
||||
isDefault: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("order field accepted", () => {
|
||||
const parsed = PersistedConfigSchema.parse({
|
||||
agents: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.61",
|
||||
"version": "0.1.60",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
]
|
||||
},
|
||||
"allowedHosts": {
|
||||
"description": "Deprecated: use hostnames instead",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
@@ -47,12 +48,9 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"injectIntoAgents": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
"additionalProperties": false
|
||||
},
|
||||
"cors": {
|
||||
"type": "object",
|
||||
@@ -127,95 +125,68 @@
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extends": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"minItems": 1
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
}
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"const": "append"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"const": "replace"
|
||||
},
|
||||
"argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"required": ["mode", "argv"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"thinkingOptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["id", "label"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "label"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalModels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PaseoConfigV1/properties/agents/properties/providers/additionalProperties/properties/models/items"
|
||||
}
|
||||
},
|
||||
"disallowedTools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"order": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -289,15 +260,6 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"turnDetection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"$ref": "#/definitions/PaseoConfigV1/properties/features/properties/dictation/properties/stt/properties/provider"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"tts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Route as DocsVoiceRouteImport } from "./routes/docs/voice";
|
||||
import { Route as DocsUpdatesRouteImport } from "./routes/docs/updates";
|
||||
import { Route as DocsSkillsRouteImport } from "./routes/docs/skills";
|
||||
import { Route as DocsSecurityRouteImport } from "./routes/docs/security";
|
||||
import { Route as DocsProvidersRouteImport } from "./routes/docs/providers";
|
||||
import { Route as DocsConfigurationRouteImport } from "./routes/docs/configuration";
|
||||
import { Route as DocsCliRouteImport } from "./routes/docs/cli";
|
||||
import { Route as DocsBestPracticesRouteImport } from "./routes/docs/best-practices";
|
||||
@@ -111,11 +110,6 @@ const DocsSecurityRoute = DocsSecurityRouteImport.update({
|
||||
path: "/security",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any);
|
||||
const DocsProvidersRoute = DocsProvidersRouteImport.update({
|
||||
id: "/providers",
|
||||
path: "/providers",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any);
|
||||
const DocsConfigurationRoute = DocsConfigurationRouteImport.update({
|
||||
id: "/configuration",
|
||||
path: "/configuration",
|
||||
@@ -151,7 +145,6 @@ export interface FileRoutesByFullPath {
|
||||
"/docs/best-practices": typeof DocsBestPracticesRoute;
|
||||
"/docs/cli": typeof DocsCliRoute;
|
||||
"/docs/configuration": typeof DocsConfigurationRoute;
|
||||
"/docs/providers": typeof DocsProvidersRoute;
|
||||
"/docs/security": typeof DocsSecurityRoute;
|
||||
"/docs/skills": typeof DocsSkillsRoute;
|
||||
"/docs/updates": typeof DocsUpdatesRoute;
|
||||
@@ -172,7 +165,6 @@ export interface FileRoutesByTo {
|
||||
"/docs/best-practices": typeof DocsBestPracticesRoute;
|
||||
"/docs/cli": typeof DocsCliRoute;
|
||||
"/docs/configuration": typeof DocsConfigurationRoute;
|
||||
"/docs/providers": typeof DocsProvidersRoute;
|
||||
"/docs/security": typeof DocsSecurityRoute;
|
||||
"/docs/skills": typeof DocsSkillsRoute;
|
||||
"/docs/updates": typeof DocsUpdatesRoute;
|
||||
@@ -196,7 +188,6 @@ export interface FileRoutesById {
|
||||
"/docs/best-practices": typeof DocsBestPracticesRoute;
|
||||
"/docs/cli": typeof DocsCliRoute;
|
||||
"/docs/configuration": typeof DocsConfigurationRoute;
|
||||
"/docs/providers": typeof DocsProvidersRoute;
|
||||
"/docs/security": typeof DocsSecurityRoute;
|
||||
"/docs/skills": typeof DocsSkillsRoute;
|
||||
"/docs/updates": typeof DocsUpdatesRoute;
|
||||
@@ -221,7 +212,6 @@ export interface FileRouteTypes {
|
||||
| "/docs/best-practices"
|
||||
| "/docs/cli"
|
||||
| "/docs/configuration"
|
||||
| "/docs/providers"
|
||||
| "/docs/security"
|
||||
| "/docs/skills"
|
||||
| "/docs/updates"
|
||||
@@ -242,7 +232,6 @@ export interface FileRouteTypes {
|
||||
| "/docs/best-practices"
|
||||
| "/docs/cli"
|
||||
| "/docs/configuration"
|
||||
| "/docs/providers"
|
||||
| "/docs/security"
|
||||
| "/docs/skills"
|
||||
| "/docs/updates"
|
||||
@@ -265,7 +254,6 @@ export interface FileRouteTypes {
|
||||
| "/docs/best-practices"
|
||||
| "/docs/cli"
|
||||
| "/docs/configuration"
|
||||
| "/docs/providers"
|
||||
| "/docs/security"
|
||||
| "/docs/skills"
|
||||
| "/docs/updates"
|
||||
@@ -401,13 +389,6 @@ declare module "@tanstack/react-router" {
|
||||
preLoaderRoute: typeof DocsSecurityRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/providers": {
|
||||
id: "/docs/providers";
|
||||
path: "/providers";
|
||||
fullPath: "/docs/providers";
|
||||
preLoaderRoute: typeof DocsProvidersRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/configuration": {
|
||||
id: "/docs/configuration";
|
||||
path: "/configuration";
|
||||
@@ -455,7 +436,6 @@ interface DocsRouteChildren {
|
||||
DocsBestPracticesRoute: typeof DocsBestPracticesRoute;
|
||||
DocsCliRoute: typeof DocsCliRoute;
|
||||
DocsConfigurationRoute: typeof DocsConfigurationRoute;
|
||||
DocsProvidersRoute: typeof DocsProvidersRoute;
|
||||
DocsSecurityRoute: typeof DocsSecurityRoute;
|
||||
DocsSkillsRoute: typeof DocsSkillsRoute;
|
||||
DocsUpdatesRoute: typeof DocsUpdatesRoute;
|
||||
@@ -468,7 +448,6 @@ const DocsRouteChildren: DocsRouteChildren = {
|
||||
DocsBestPracticesRoute: DocsBestPracticesRoute,
|
||||
DocsCliRoute: DocsCliRoute,
|
||||
DocsConfigurationRoute: DocsConfigurationRoute,
|
||||
DocsProvidersRoute: DocsProvidersRoute,
|
||||
DocsSecurityRoute: DocsSecurityRoute,
|
||||
DocsSkillsRoute: DocsSkillsRoute,
|
||||
DocsUpdatesRoute: DocsUpdatesRoute,
|
||||
|
||||
@@ -12,7 +12,6 @@ const navigation = [
|
||||
{ name: "Git worktrees", href: "/docs/worktrees" },
|
||||
{ name: "CLI", href: "/docs/cli" },
|
||||
{ name: "Skills", href: "/docs/skills" },
|
||||
{ name: "Providers", href: "/docs/providers" },
|
||||
{ name: "Configuration", href: "/docs/configuration" },
|
||||
{ name: "Security", href: "/docs/security" },
|
||||
{ name: "Best practices", href: "/docs/best-practices" },
|
||||
|
||||
@@ -11,8 +11,6 @@ export const Route = createFileRoute("/docs/configuration")({
|
||||
component: Configuration,
|
||||
});
|
||||
|
||||
const CUSTOM_PROVIDERS_URL = "https://github.com/getpaseo/paseo/blob/main/docs/CUSTOM-PROVIDERS.md";
|
||||
|
||||
function Configuration() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -61,12 +59,15 @@ function Configuration() {
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Example</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Minimal example that configures listening address, hostnames, and MCP:
|
||||
Minimal example that configures listening address, hostnames, provider keys, and MCP:
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"$schema": "https://paseo.sh/schemas/paseo.config.v1.json",
|
||||
"version": 1,
|
||||
"providers": {
|
||||
"openai": { "apiKey": "..." }
|
||||
},
|
||||
"daemon": {
|
||||
"listen": "127.0.0.1:6767",
|
||||
"hostnames": ["localhost", ".localhost"],
|
||||
@@ -82,31 +83,84 @@ function Configuration() {
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agent providers</h2>
|
||||
<h2 className="text-xl font-medium">Agent provider runtime settings</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Agent providers — both the first-class ones Paseo ships with and custom entries you add
|
||||
under <code className="font-mono">agents.providers</code> — are documented on their own
|
||||
page.
|
||||
Use <code className="font-mono">agents.providers</code> to customize how Paseo launches
|
||||
agent provider CLIs. This works for <code className="font-mono">claude</code>,{" "}
|
||||
<code className="font-mono">codex</code>, and
|
||||
<code className="font-mono"> opencode</code>.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
See{" "}
|
||||
<a href="/docs/providers" className="underline hover:text-white/80">
|
||||
Providers
|
||||
</a>{" "}
|
||||
for first-class providers, how to point Claude at Anthropic-compatible endpoints (Z.AI,
|
||||
Alibaba/Qwen), multiple profiles, custom binaries, ACP agents, and the{" "}
|
||||
<code className="font-mono">additionalModels</code> merge behavior. The full field
|
||||
reference lives on GitHub at{" "}
|
||||
<a
|
||||
href={CUSTOM_PROVIDERS_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
docs/CUSTOM-PROVIDERS.md
|
||||
</a>
|
||||
.
|
||||
<code className="font-mono">command.mode</code> can be{" "}
|
||||
<code className="font-mono">default</code>, <code className="font-mono">append</code>, or{" "}
|
||||
<code className="font-mono">replace</code>. Use <code className="font-mono">env</code> to
|
||||
inject provider-specific environment variables.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium">Enable Claude Code Chrome MCP</h3>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"command": {
|
||||
"mode": "append",
|
||||
"args": ["--chrome"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<h3 className="text-lg font-medium">
|
||||
Point Claude to Anthropic-compatible endpoints (z.ai example)
|
||||
</h3>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"ANTHROPIC_AUTH_TOKEN": "auth token",
|
||||
"ANTHROPIC_API_KEY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<h3 className="text-lg font-medium">Run Claude through Docker</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Create a wrapper script that runs Claude in Docker, then tell Paseo to replace the Claude
|
||||
launch command with that script.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"command": {
|
||||
"mode": "replace",
|
||||
"argv": ["/Users/you/bin/claude-docker"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
docker run --rm -i \\
|
||||
-v "$PWD":"$PWD" \\
|
||||
-w "$PWD" \\
|
||||
-v "$HOME/.claude":"$HOME/.claude" \\
|
||||
ghcr.io/anthropics/claude-code:latest \\
|
||||
claude "$@"`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
|
||||
@@ -148,11 +148,6 @@ function GettingStarted() {
|
||||
Voice
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/providers" className="underline hover:text-white/80">
|
||||
Providers
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs/configuration" className="underline hover:text-white/80">
|
||||
Configuration
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { pageMeta } from "~/meta";
|
||||
|
||||
export const Route = createFileRoute("/docs/providers")({
|
||||
head: () => ({
|
||||
meta: pageMeta(
|
||||
"Providers - Paseo Docs",
|
||||
"First-class agent providers in Paseo, and how to configure custom providers, ACP agents, and profiles.",
|
||||
),
|
||||
}),
|
||||
component: Providers,
|
||||
});
|
||||
|
||||
const CUSTOM_PROVIDERS_URL = "https://github.com/getpaseo/paseo/blob/main/docs/CUSTOM-PROVIDERS.md";
|
||||
|
||||
function Providers() {
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium font-title mb-4">Providers</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
A provider is an agent CLI that Paseo knows how to launch, stream, and control. Paseo
|
||||
ships with first-class providers for the major coding agents, and lets you add your own
|
||||
through <code className="font-mono">config.json</code> — either by pointing an existing
|
||||
provider at a different API, adding extra profiles, or plugging in any{" "}
|
||||
<a
|
||||
href="https://agentclientprotocol.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
ACP
|
||||
</a>
|
||||
-compatible agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">First-class providers</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
These work out of the box once the underlying CLI is installed and authenticated. Paseo
|
||||
discovers them automatically, wires up modes, and exposes them in the app and CLI.
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-3 list-disc list-inside">
|
||||
<li>
|
||||
<code className="font-mono">claude</code> — Anthropic's Claude Code. Multi-tool
|
||||
assistant with MCP support, streaming, and deep reasoning.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">codex</code> — OpenAI's Codex workspace agent with sandbox
|
||||
controls and optional network access.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">opencode</code> — Open-source coding assistant with
|
||||
multi-provider model support.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">copilot</code> — GitHub Copilot via ACP, with dynamic modes
|
||||
and session support.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">pi</code> — Minimal terminal-based coding agent with
|
||||
multi-provider LLM support.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Custom providers</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Everything beyond the defaults lives under{" "}
|
||||
<code className="font-mono">agents.providers</code> in{" "}
|
||||
<code className="font-mono">~/.paseo/config.json</code>. You can:
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<strong>Extend</strong> a first-class provider to point at a different API (Z.AI,
|
||||
Alibaba/Qwen, a proxy, a self-hosted endpoint).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Add profiles</strong> — multiple entries against the same underlying provider
|
||||
with different credentials or curated model lists.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Override the binary</strong> — run a nightly build, a wrapper script, or a
|
||||
Docker image instead of the installed CLI.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Add ACP agents</strong> — Gemini CLI, Hermes, or any agent speaking the Agent
|
||||
Client Protocol over stdio.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Disable</strong> a provider you don't use.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Provider IDs must be lowercase alphanumeric with hyphens (
|
||||
<code className="font-mono">/^[a-z][a-z0-9-]*$/</code>). Every custom entry needs{" "}
|
||||
<code className="font-mono">extends</code> (a first-class provider ID or{" "}
|
||||
<code className="font-mono">"acp"</code>) and a <code className="font-mono">label</code>.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The examples below are a quick tour. The full, up-to-date reference is on GitHub:{" "}
|
||||
<a
|
||||
href={CUSTOM_PROVIDERS_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
docs/CUSTOM-PROVIDERS.md
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Extending a first-class provider</h2>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"my-claude": {
|
||||
"extends": "claude",
|
||||
"label": "My Claude",
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "sk-ant-...",
|
||||
"ANTHROPIC_BASE_URL": "https://my-proxy.example.com/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Z.AI (GLM) coding plan</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Z.AI exposes GLM models through an Anthropic-compatible endpoint. Point{" "}
|
||||
<code className="font-mono">ANTHROPIC_BASE_URL</code> at their API and use{" "}
|
||||
<code className="font-mono">ANTHROPIC_AUTH_TOKEN</code> for the key. Third-party endpoints
|
||||
don't support Anthropic's server-side tools, so disable{" "}
|
||||
<code className="font-mono">WebSearch</code>.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"zai": {
|
||||
"extends": "claude",
|
||||
"label": "ZAI",
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "<your-zai-api-key>",
|
||||
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
|
||||
"API_TIMEOUT_MS": "3000000"
|
||||
},
|
||||
"disallowedTools": ["WebSearch"],
|
||||
"models": [
|
||||
{ "id": "glm-5-turbo", "label": "GLM 5 Turbo", "isDefault": true },
|
||||
{ "id": "glm-5.1", "label": "GLM 5.1" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Alibaba Cloud (Qwen) coding plan</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Alibaba's coding plan routes Claude Code to Qwen models via an Anthropic-compatible API.
|
||||
Subscription keys look like <code className="font-mono">sk-sp-...</code> and must be
|
||||
created in the Singapore region.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"qwen": {
|
||||
"extends": "claude",
|
||||
"label": "Qwen (Alibaba)",
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-sp-<coding-plan-key>",
|
||||
"ANTHROPIC_BASE_URL": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||
},
|
||||
"disallowedTools": ["WebSearch"],
|
||||
"models": [
|
||||
{ "id": "qwen3.5-plus", "label": "Qwen 3.5 Plus", "isDefault": true },
|
||||
{ "id": "qwen3-coder-next", "label": "Qwen 3 Coder Next" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Multiple profiles</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Create as many entries as you want against the same first-class provider. Each one shows
|
||||
up as a separate option in the app with its own credentials and models.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude-work": {
|
||||
"extends": "claude",
|
||||
"label": "Claude (Work)",
|
||||
"env": { "ANTHROPIC_API_KEY": "sk-ant-work-..." }
|
||||
},
|
||||
"claude-personal": {
|
||||
"extends": "claude",
|
||||
"label": "Claude (Personal)",
|
||||
"env": { "ANTHROPIC_API_KEY": "sk-ant-personal-..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Custom binary</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
<code className="font-mono">command</code> is an array — first element is the binary, the
|
||||
rest are arguments. It fully replaces the default launch command for that provider.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"claude": {
|
||||
"command": ["/opt/claude-nightly/claude"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">ACP providers</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Any agent that speaks{" "}
|
||||
<a
|
||||
href="https://agentclientprotocol.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
ACP
|
||||
</a>{" "}
|
||||
over stdio can be added with <code className="font-mono">extends: "acp"</code> and a{" "}
|
||||
<code className="font-mono">command</code>. Paseo spawns the process, sends an{" "}
|
||||
<code className="font-mono">initialize</code> JSON-RPC request, and the agent reports its
|
||||
capabilities, modes, and models at runtime.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"gemini": {
|
||||
"extends": "acp",
|
||||
"label": "Google Gemini",
|
||||
"command": ["gemini", "--acp"]
|
||||
},
|
||||
"hermes": {
|
||||
"extends": "acp",
|
||||
"label": "Hermes",
|
||||
"command": ["hermes", "acp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Adding or relabeling models</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
<code className="font-mono">models</code> replaces the model list entirely.{" "}
|
||||
<code className="font-mono">additionalModels</code> merges with runtime-discovered models
|
||||
(ACP) or with <code className="font-mono">models</code> — use it to add an extra entry or
|
||||
relabel a discovered one without redeclaring the full list. An entry with the same{" "}
|
||||
<code className="font-mono">id</code> as a discovered model updates it in place.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"gemini": {
|
||||
"extends": "acp",
|
||||
"label": "Google Gemini",
|
||||
"command": ["gemini", "--acp"],
|
||||
"additionalModels": [
|
||||
{ "id": "experimental-model", "label": "Experimental", "isDefault": true },
|
||||
{ "id": "gemini-2.5-pro", "label": "Gemini 2.5 Pro (preferred)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Disabling a provider</h2>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
{`{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"copilot": { "enabled": false }
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Full reference</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
For the complete field reference (<code className="font-mono">extends</code>,{" "}
|
||||
<code className="font-mono">label</code>, <code className="font-mono">command</code>,{" "}
|
||||
<code className="font-mono">env</code>, <code className="font-mono">models</code>,{" "}
|
||||
<code className="font-mono">additionalModels</code>,{" "}
|
||||
<code className="font-mono">disallowedTools</code>,{" "}
|
||||
<code className="font-mono">enabled</code>, <code className="font-mono">order</code>),
|
||||
model and thinking-option schemas, and deeper examples for each plan, see{" "}
|
||||
<a
|
||||
href={CUSTOM_PROVIDERS_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:text-white/80"
|
||||
>
|
||||
docs/CUSTOM-PROVIDERS.md
|
||||
</a>{" "}
|
||||
on GitHub.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,6 @@ const sitemapPages = [
|
||||
"/docs/best-practices",
|
||||
"/docs/cli",
|
||||
"/docs/configuration",
|
||||
"/docs/providers",
|
||||
"/docs/skills",
|
||||
"/docs/security",
|
||||
"/docs/updates",
|
||||
|
||||
Reference in New Issue
Block a user