Skip to content

Commit a724bbc

Browse files
authored
feat: add bundled Chutes extension (#49136)
* refactor: generalize bundled provider discovery seams * feat: land chutes extension via plugin-owned auth (#41416) (thanks @Veightor)
1 parent ea15819 commit a724bbc

31 files changed

Lines changed: 1856 additions & 171 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
3535
- Plugins/bundles: make enabled bundle MCP servers expose runnable tools in embedded Pi, and default relative bundle MCP launches to the bundle root so marketplace bundles like Context7 work through Pi instead of stopping at config import.
3636
- Scope message SecretRef resolution and harden doctor/status paths. (#48728) Thanks @joshavant.
3737
- Plugins/testing: add a public `openclaw/plugin-sdk/testing` seam for plugin-author test helpers, and move bundled-extension-only test bridges out of `extensions/` into private repo test helpers.
38+
- Plugins/Chutes: add a bundled Chutes provider with plugin-owned OAuth/API-key auth, dynamic model discovery, and default-on extension wiring. (#41416) Thanks @Veightor.
3839

3940
### Breaking
4041

extensions/chutes/index.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { definePluginEntry } from "openclaw/plugin-sdk/core";
2+
import {
3+
buildOauthProviderAuthResult,
4+
createProviderApiKeyAuthMethod,
5+
loginChutes,
6+
resolveOAuthApiKeyMarker,
7+
type ProviderAuthContext,
8+
type ProviderAuthResult,
9+
} from "openclaw/plugin-sdk/provider-auth";
10+
import {
11+
CHUTES_DEFAULT_MODEL_REF,
12+
applyChutesApiKeyConfig,
13+
applyChutesProviderConfig,
14+
} from "./onboard.js";
15+
import { buildChutesProvider } from "./provider-catalog.js";
16+
17+
const PROVIDER_ID = "chutes";
18+
19+
async function runChutesOAuth(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
20+
const isRemote = ctx.isRemote;
21+
const redirectUri =
22+
process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() || "http://127.0.0.1:1456/oauth-callback";
23+
const scopes = process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke";
24+
const clientId =
25+
process.env.CHUTES_CLIENT_ID?.trim() ||
26+
String(
27+
await ctx.prompter.text({
28+
message: "Enter Chutes OAuth client id",
29+
placeholder: "cid_xxx",
30+
validate: (value: string) => (value?.trim() ? undefined : "Required"),
31+
}),
32+
).trim();
33+
const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined;
34+
35+
await ctx.prompter.note(
36+
isRemote
37+
? [
38+
"You are running in a remote/VPS environment.",
39+
"A URL will be shown for you to open in your LOCAL browser.",
40+
"After signing in, paste the redirect URL back here.",
41+
"",
42+
`Redirect URI: ${redirectUri}`,
43+
].join("\n")
44+
: [
45+
"Browser will open for Chutes authentication.",
46+
"If the callback doesn't auto-complete, paste the redirect URL.",
47+
"",
48+
`Redirect URI: ${redirectUri}`,
49+
].join("\n"),
50+
"Chutes OAuth",
51+
);
52+
53+
const progress = ctx.prompter.progress("Starting Chutes OAuth…");
54+
try {
55+
const { onAuth, onPrompt } = ctx.oauth.createVpsAwareHandlers({
56+
isRemote,
57+
prompter: ctx.prompter,
58+
runtime: ctx.runtime,
59+
spin: progress,
60+
openUrl: ctx.openUrl,
61+
localBrowserMessage: "Complete sign-in in browser…",
62+
});
63+
64+
const creds = await loginChutes({
65+
app: {
66+
clientId,
67+
clientSecret,
68+
redirectUri,
69+
scopes: scopes.split(/\s+/).filter(Boolean),
70+
},
71+
manual: isRemote,
72+
onAuth,
73+
onPrompt,
74+
onProgress: (message) => progress.update(message),
75+
});
76+
77+
progress.stop("Chutes OAuth complete");
78+
79+
return buildOauthProviderAuthResult({
80+
providerId: PROVIDER_ID,
81+
defaultModel: CHUTES_DEFAULT_MODEL_REF,
82+
access: creds.access,
83+
refresh: creds.refresh,
84+
expires: creds.expires,
85+
email: typeof creds.email === "string" ? creds.email : undefined,
86+
credentialExtra: {
87+
clientId,
88+
...("accountId" in creds && typeof creds.accountId === "string"
89+
? { accountId: creds.accountId }
90+
: {}),
91+
},
92+
configPatch: applyChutesProviderConfig({}),
93+
notes: [
94+
"Chutes OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.",
95+
`Redirect URI: ${redirectUri}`,
96+
],
97+
});
98+
} catch (err) {
99+
progress.stop("Chutes OAuth failed");
100+
await ctx.prompter.note(
101+
[
102+
"Trouble with OAuth?",
103+
"Verify CHUTES_CLIENT_ID (and CHUTES_CLIENT_SECRET if required).",
104+
`Verify the OAuth app redirect URI includes: ${redirectUri}`,
105+
"Chutes docs: https://chutes.ai/docs/sign-in-with-chutes/overview",
106+
].join("\n"),
107+
"OAuth help",
108+
);
109+
throw err;
110+
}
111+
}
112+
113+
export default definePluginEntry({
114+
id: PROVIDER_ID,
115+
name: "Chutes Provider",
116+
description: "Bundled Chutes.ai provider plugin",
117+
register(api) {
118+
api.registerProvider({
119+
id: PROVIDER_ID,
120+
label: "Chutes",
121+
docsPath: "/providers/chutes",
122+
envVars: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
123+
auth: [
124+
{
125+
id: "oauth",
126+
label: "Chutes OAuth",
127+
hint: "Browser sign-in",
128+
kind: "oauth",
129+
wizard: {
130+
choiceId: "chutes",
131+
choiceLabel: "Chutes (OAuth)",
132+
choiceHint: "Browser sign-in",
133+
groupId: "chutes",
134+
groupLabel: "Chutes",
135+
groupHint: "OAuth + API key",
136+
},
137+
run: async (ctx) => await runChutesOAuth(ctx),
138+
},
139+
createProviderApiKeyAuthMethod({
140+
providerId: PROVIDER_ID,
141+
methodId: "api-key",
142+
label: "Chutes API key",
143+
hint: "Open-source models including Llama, DeepSeek, and more",
144+
optionKey: "chutesApiKey",
145+
flagName: "--chutes-api-key",
146+
envVar: "CHUTES_API_KEY",
147+
promptMessage: "Enter Chutes API key",
148+
noteTitle: "Chutes",
149+
noteMessage: [
150+
"Chutes provides access to leading open-source models including Llama, DeepSeek, and more.",
151+
"Get your API key at: https://chutes.ai/settings/api-keys",
152+
].join("\n"),
153+
defaultModel: CHUTES_DEFAULT_MODEL_REF,
154+
expectedProviders: ["chutes"],
155+
applyConfig: (cfg) => applyChutesApiKeyConfig(cfg),
156+
wizard: {
157+
choiceId: "chutes-api-key",
158+
choiceLabel: "Chutes API key",
159+
groupId: "chutes",
160+
groupLabel: "Chutes",
161+
groupHint: "OAuth + API key",
162+
},
163+
}),
164+
],
165+
catalog: {
166+
order: "profile",
167+
run: async (ctx) => {
168+
const { apiKey, discoveryApiKey } = ctx.resolveProviderAuth(PROVIDER_ID, {
169+
oauthMarker: resolveOAuthApiKeyMarker(PROVIDER_ID),
170+
});
171+
if (!apiKey) {
172+
return null;
173+
}
174+
return {
175+
provider: {
176+
...(await buildChutesProvider(discoveryApiKey)),
177+
apiKey,
178+
},
179+
};
180+
},
181+
},
182+
});
183+
},
184+
});

extensions/chutes/onboard.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
CHUTES_BASE_URL,
3+
CHUTES_DEFAULT_MODEL_REF,
4+
CHUTES_MODEL_CATALOG,
5+
buildChutesModelDefinition,
6+
} from "openclaw/plugin-sdk/provider-models";
7+
import {
8+
applyAgentDefaultModelPrimary,
9+
applyProviderConfigWithModelCatalog,
10+
type OpenClawConfig,
11+
} from "openclaw/plugin-sdk/provider-onboard";
12+
13+
export { CHUTES_DEFAULT_MODEL_REF };
14+
15+
/**
16+
* Apply Chutes provider configuration without changing the default model.
17+
* Registers all catalog models and sets provider aliases (chutes-fast, etc.).
18+
*/
19+
export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
20+
const models = { ...cfg.agents?.defaults?.models };
21+
for (const m of CHUTES_MODEL_CATALOG) {
22+
models[`chutes/${m.id}`] = {
23+
...models[`chutes/${m.id}`],
24+
};
25+
}
26+
27+
models["chutes-fast"] = { alias: "chutes/zai-org/GLM-4.7-FP8" };
28+
models["chutes-vision"] = { alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506" };
29+
models["chutes-pro"] = { alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" };
30+
31+
const chutesModels = CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition);
32+
return applyProviderConfigWithModelCatalog(cfg, {
33+
agentModels: models,
34+
providerId: "chutes",
35+
api: "openai-completions",
36+
baseUrl: CHUTES_BASE_URL,
37+
catalogModels: chutesModels,
38+
});
39+
}
40+
41+
/**
42+
* Apply Chutes provider configuration AND set Chutes as the default model.
43+
*/
44+
export function applyChutesConfig(cfg: OpenClawConfig): OpenClawConfig {
45+
const next = applyChutesProviderConfig(cfg);
46+
return {
47+
...next,
48+
agents: {
49+
...next.agents,
50+
defaults: {
51+
...next.agents?.defaults,
52+
model: {
53+
primary: CHUTES_DEFAULT_MODEL_REF,
54+
fallbacks: ["chutes/deepseek-ai/DeepSeek-V3.2-TEE", "chutes/Qwen/Qwen3-32B"],
55+
},
56+
imageModel: {
57+
primary: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506",
58+
fallbacks: ["chutes/chutesai/Mistral-Small-3.1-24B-Instruct-2503"],
59+
},
60+
},
61+
},
62+
};
63+
}
64+
65+
export function applyChutesApiKeyConfig(cfg: OpenClawConfig): OpenClawConfig {
66+
return applyAgentDefaultModelPrimary(applyChutesProviderConfig(cfg), CHUTES_DEFAULT_MODEL_REF);
67+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"id": "chutes",
3+
"enabledByDefault": true,
4+
"providers": ["chutes"],
5+
"providerAuthEnvVars": {
6+
"chutes": ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"]
7+
},
8+
"providerAuthChoices": [
9+
{
10+
"provider": "chutes",
11+
"method": "oauth",
12+
"choiceId": "chutes",
13+
"choiceLabel": "Chutes (OAuth)",
14+
"choiceHint": "Browser sign-in",
15+
"groupId": "chutes",
16+
"groupLabel": "Chutes",
17+
"groupHint": "OAuth + API key"
18+
},
19+
{
20+
"provider": "chutes",
21+
"method": "api-key",
22+
"choiceId": "chutes-api-key",
23+
"choiceLabel": "Chutes API key",
24+
"choiceHint": "Open-source models including Llama, DeepSeek, and more",
25+
"groupId": "chutes",
26+
"groupLabel": "Chutes",
27+
"groupHint": "OAuth + API key",
28+
"optionKey": "chutesApiKey",
29+
"cliFlag": "--chutes-api-key",
30+
"cliOption": "--chutes-api-key <key>",
31+
"cliDescription": "Chutes API key"
32+
}
33+
],
34+
"configSchema": {
35+
"type": "object",
36+
"additionalProperties": false,
37+
"properties": {}
38+
}
39+
}

extensions/chutes/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@openclaw/chutes-provider",
3+
"version": "2026.3.17",
4+
"private": true,
5+
"description": "OpenClaw Chutes.ai provider plugin",
6+
"type": "module",
7+
"openclaw": {
8+
"extensions": [
9+
"./index.ts"
10+
]
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
CHUTES_BASE_URL,
3+
CHUTES_MODEL_CATALOG,
4+
buildChutesModelDefinition,
5+
discoverChutesModels,
6+
type ModelProviderConfig,
7+
} from "openclaw/plugin-sdk/provider-models";
8+
9+
/**
10+
* Build the Chutes provider with dynamic model discovery.
11+
* Falls back to the static catalog on failure.
12+
* Accepts an optional access token (API key or OAuth access token) for authenticated discovery.
13+
*/
14+
export async function buildChutesProvider(accessToken?: string): Promise<ModelProviderConfig> {
15+
const models = await discoverChutesModels(accessToken);
16+
return {
17+
baseUrl: CHUTES_BASE_URL,
18+
api: "openai-completions",
19+
models: models.length > 0 ? models : CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
20+
};
21+
}

0 commit comments

Comments
 (0)