@openclaw/chutes-provider 0.0.0 → 2026.6.9-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/api.js +4 -0
- package/dist/index.js +149 -0
- package/dist/model-discovery-env.js +10 -0
- package/dist/models.js +766 -0
- package/dist/oauth.js +144 -0
- package/dist/onboard.js +62 -0
- package/dist/provider-catalog.js +25 -0
- package/npm-shrinkwrap.json +12 -0
- package/openclaw.plugin.json +737 -0
- package/package.json +44 -8
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/provider-auth";
|
|
2
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { resolveExpiresAtMsFromDurationSeconds } from "openclaw/plugin-sdk/number-runtime";
|
|
5
|
+
import { parseOAuthCallbackInput, waitForLocalOAuthCallback } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
6
|
+
//#region extensions/chutes/oauth.ts
|
|
7
|
+
/**
|
|
8
|
+
* Chutes OAuth PKCE login flow.
|
|
9
|
+
*/
|
|
10
|
+
const CHUTES_AUTHORIZE_ENDPOINT = "https://api.chutes.ai/idp/authorize";
|
|
11
|
+
const CHUTES_TOKEN_ENDPOINT = "https://api.chutes.ai/idp/token";
|
|
12
|
+
const CHUTES_USERINFO_ENDPOINT = "https://api.chutes.ai/idp/userinfo";
|
|
13
|
+
function parseRedirectUri(redirectUri) {
|
|
14
|
+
const url = new URL(redirectUri);
|
|
15
|
+
if (url.protocol !== "http:") throw new Error(`Chutes OAuth redirect URI must be http:// (got ${redirectUri})`);
|
|
16
|
+
const hostname = url.hostname || "127.0.0.1";
|
|
17
|
+
if (hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "::1") throw new Error(`Chutes OAuth redirect hostname must be loopback (got ${hostname}). Use http://127.0.0.1:<port>/...`);
|
|
18
|
+
return {
|
|
19
|
+
hostname,
|
|
20
|
+
port: url.port ? Number.parseInt(url.port, 10) : 80,
|
|
21
|
+
pathname: url.pathname || "/"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function parseManualOAuthInput(input, expectedState) {
|
|
25
|
+
const parsed = parseOAuthCallbackInput(input, {
|
|
26
|
+
invalidInput: "Paste the full redirect URL (must include code + state).",
|
|
27
|
+
missingState: "Missing 'state' parameter. Paste the full redirect URL."
|
|
28
|
+
});
|
|
29
|
+
if ("error" in parsed) throw new Error(parsed.error);
|
|
30
|
+
if (parsed.state !== expectedState) throw new Error("OAuth state mismatch - possible CSRF attack. Please retry login.");
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
function buildAuthorizeUrl(params) {
|
|
34
|
+
return `${CHUTES_AUTHORIZE_ENDPOINT}?${new URLSearchParams({
|
|
35
|
+
client_id: params.clientId,
|
|
36
|
+
redirect_uri: params.redirectUri,
|
|
37
|
+
response_type: "code",
|
|
38
|
+
scope: params.scopes.join(" "),
|
|
39
|
+
state: params.state,
|
|
40
|
+
code_challenge: params.challenge,
|
|
41
|
+
code_challenge_method: "S256"
|
|
42
|
+
}).toString()}`;
|
|
43
|
+
}
|
|
44
|
+
function resolveChutesExpiresAt(value, now) {
|
|
45
|
+
return resolveExpiresAtMsFromDurationSeconds(value, {
|
|
46
|
+
nowMs: now,
|
|
47
|
+
bufferMs: 300 * 1e3,
|
|
48
|
+
minRemainingMs: 3e4
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function fetchChutesUserInfo(params) {
|
|
52
|
+
const response = await (params.fetchFn ?? fetch)(CHUTES_USERINFO_ENDPOINT, { headers: { Authorization: `Bearer ${params.accessToken}` } });
|
|
53
|
+
if (!response.ok) return null;
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
return data && typeof data === "object" ? data : null;
|
|
56
|
+
}
|
|
57
|
+
async function exchangeChutesCodeForTokens(params) {
|
|
58
|
+
const fetchFn = params.fetchFn ?? fetch;
|
|
59
|
+
const now = params.now ?? Date.now();
|
|
60
|
+
const body = new URLSearchParams(toFormUrlEncoded({
|
|
61
|
+
grant_type: "authorization_code",
|
|
62
|
+
client_id: params.app.clientId,
|
|
63
|
+
code: params.code,
|
|
64
|
+
redirect_uri: params.app.redirectUri,
|
|
65
|
+
code_verifier: params.codeVerifier
|
|
66
|
+
}));
|
|
67
|
+
if (params.app.clientSecret) body.set("client_secret", params.app.clientSecret);
|
|
68
|
+
const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
71
|
+
body
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) throw new Error(`Chutes token exchange failed: ${await response.text()}`);
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
const access = normalizeOptionalString(data.access_token);
|
|
76
|
+
const refresh = normalizeOptionalString(data.refresh_token);
|
|
77
|
+
const expires = resolveChutesExpiresAt(data.expires_in, now);
|
|
78
|
+
if (!access) throw new Error("Chutes token exchange returned no access_token");
|
|
79
|
+
if (!refresh) throw new Error("Chutes token exchange returned no refresh_token");
|
|
80
|
+
if (expires === void 0) throw new Error("Chutes token exchange returned invalid expires_in");
|
|
81
|
+
const info = await fetchChutesUserInfo({
|
|
82
|
+
accessToken: access,
|
|
83
|
+
fetchFn
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
access,
|
|
87
|
+
refresh,
|
|
88
|
+
expires,
|
|
89
|
+
email: info?.username,
|
|
90
|
+
accountId: info?.sub,
|
|
91
|
+
clientId: params.app.clientId
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Runs Chutes OAuth and returns refreshable stored credentials. */
|
|
95
|
+
async function loginChutes(params) {
|
|
96
|
+
const { verifier, challenge } = generatePkceVerifierChallenge();
|
|
97
|
+
const state = params.createState?.() ?? randomBytes(16).toString("hex");
|
|
98
|
+
const timeoutMs = params.timeoutMs ?? 180 * 1e3;
|
|
99
|
+
const url = buildAuthorizeUrl({
|
|
100
|
+
clientId: params.app.clientId,
|
|
101
|
+
redirectUri: params.app.redirectUri,
|
|
102
|
+
scopes: params.app.scopes,
|
|
103
|
+
state,
|
|
104
|
+
challenge
|
|
105
|
+
});
|
|
106
|
+
let codeAndState;
|
|
107
|
+
if (params.manual) {
|
|
108
|
+
await params.onAuth({ url });
|
|
109
|
+
params.onProgress?.("Waiting for redirect URL...");
|
|
110
|
+
codeAndState = parseManualOAuthInput(await params.onPrompt({
|
|
111
|
+
message: "Paste the redirect URL",
|
|
112
|
+
placeholder: `${params.app.redirectUri}?code=...&state=...`
|
|
113
|
+
}), state);
|
|
114
|
+
} else {
|
|
115
|
+
const redirect = parseRedirectUri(params.app.redirectUri);
|
|
116
|
+
const callback = waitForLocalOAuthCallback({
|
|
117
|
+
expectedState: state,
|
|
118
|
+
timeoutMs,
|
|
119
|
+
port: redirect.port,
|
|
120
|
+
callbackPath: redirect.pathname,
|
|
121
|
+
redirectUri: params.app.redirectUri,
|
|
122
|
+
successTitle: "Chutes OAuth complete",
|
|
123
|
+
hostname: redirect.hostname,
|
|
124
|
+
onProgress: params.onProgress
|
|
125
|
+
}).catch(async () => {
|
|
126
|
+
params.onProgress?.("OAuth callback not detected; paste redirect URL...");
|
|
127
|
+
return parseManualOAuthInput(await params.onPrompt({
|
|
128
|
+
message: "Paste the redirect URL",
|
|
129
|
+
placeholder: `${params.app.redirectUri}?code=...&state=...`
|
|
130
|
+
}), state);
|
|
131
|
+
});
|
|
132
|
+
await params.onAuth({ url });
|
|
133
|
+
codeAndState = await callback;
|
|
134
|
+
}
|
|
135
|
+
params.onProgress?.("Exchanging code for tokens...");
|
|
136
|
+
return await exchangeChutesCodeForTokens({
|
|
137
|
+
app: params.app,
|
|
138
|
+
code: codeAndState.code,
|
|
139
|
+
codeVerifier: verifier,
|
|
140
|
+
fetchFn: params.fetchFn
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
export { loginChutes };
|
package/dist/onboard.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CHUTES_BASE_URL, CHUTES_DEFAULT_MODEL_REF, CHUTES_MODEL_CATALOG, buildChutesModelDefinition } from "./models.js";
|
|
2
|
+
import { applyAgentDefaultModelPrimary, applyProviderConfigWithModelCatalogPreset } from "openclaw/plugin-sdk/provider-onboard";
|
|
3
|
+
//#region extensions/chutes/onboard.ts
|
|
4
|
+
/**
|
|
5
|
+
* Chutes onboarding config helpers for OAuth and API-key setup.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Apply Chutes provider configuration without changing the default model.
|
|
9
|
+
* Registers all catalog models and sets provider aliases (chutes-fast, etc.).
|
|
10
|
+
*/
|
|
11
|
+
function applyChutesProviderConfig(cfg) {
|
|
12
|
+
return applyProviderConfigWithModelCatalogPreset(cfg, {
|
|
13
|
+
providerId: "chutes",
|
|
14
|
+
api: "openai-completions",
|
|
15
|
+
baseUrl: CHUTES_BASE_URL,
|
|
16
|
+
catalogModels: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
|
|
17
|
+
aliases: [
|
|
18
|
+
...CHUTES_MODEL_CATALOG.map((model) => `chutes/${model.id}`),
|
|
19
|
+
{
|
|
20
|
+
modelRef: "chutes-fast",
|
|
21
|
+
alias: "chutes/zai-org/GLM-4.7-FP8"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
modelRef: "chutes-vision",
|
|
25
|
+
alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
modelRef: "chutes-pro",
|
|
29
|
+
alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Apply Chutes provider configuration AND set Chutes as the default model.
|
|
36
|
+
*/
|
|
37
|
+
function applyChutesConfig(cfg) {
|
|
38
|
+
const next = applyChutesProviderConfig(cfg);
|
|
39
|
+
return {
|
|
40
|
+
...next,
|
|
41
|
+
agents: {
|
|
42
|
+
...next.agents,
|
|
43
|
+
defaults: {
|
|
44
|
+
...next.agents?.defaults,
|
|
45
|
+
model: {
|
|
46
|
+
primary: CHUTES_DEFAULT_MODEL_REF,
|
|
47
|
+
fallbacks: ["chutes/deepseek-ai/DeepSeek-V3.2-TEE", "chutes/Qwen/Qwen3-32B"]
|
|
48
|
+
},
|
|
49
|
+
imageModel: {
|
|
50
|
+
primary: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506",
|
|
51
|
+
fallbacks: ["chutes/chutesai/Mistral-Small-3.1-24B-Instruct-2503"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Applies Chutes provider config and sets the default model for API-key auth. */
|
|
58
|
+
function applyChutesApiKeyConfig(cfg) {
|
|
59
|
+
return applyAgentDefaultModelPrimary(applyChutesProviderConfig(cfg), CHUTES_DEFAULT_MODEL_REF);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { CHUTES_DEFAULT_MODEL_REF, applyChutesApiKeyConfig, applyChutesConfig, applyChutesProviderConfig };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CHUTES_BASE_URL, CHUTES_MODEL_CATALOG, buildChutesModelDefinition, discoverChutesModels } from "./models.js";
|
|
2
|
+
//#region extensions/chutes/provider-catalog.ts
|
|
3
|
+
/** Builds the static Chutes provider catalog from bundled model metadata. */
|
|
4
|
+
function buildStaticChutesProvider() {
|
|
5
|
+
return {
|
|
6
|
+
baseUrl: CHUTES_BASE_URL,
|
|
7
|
+
api: "openai-completions",
|
|
8
|
+
models: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build the Chutes provider with dynamic model discovery.
|
|
13
|
+
* Falls back to the static catalog on failure.
|
|
14
|
+
* Accepts an optional access token (API key or OAuth access token) for authenticated discovery.
|
|
15
|
+
*/
|
|
16
|
+
async function buildChutesProvider(accessToken) {
|
|
17
|
+
const models = await discoverChutesModels(accessToken);
|
|
18
|
+
return {
|
|
19
|
+
baseUrl: CHUTES_BASE_URL,
|
|
20
|
+
api: "openai-completions",
|
|
21
|
+
models: models.length > 0 ? models : CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { buildChutesProvider, buildStaticChutesProvider };
|