Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# OpenClaw Chutes Provider
Official OpenClaw provider plugin for Chutes.
Install from OpenClaw:
```bash
openclaw plugins install @openclaw/chutes-provider
openclaw gateway restart
```
See <https://docs.openclaw.ai/providers/chutes> for setup and configuration.

17
extensions/chutes/api.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* Public Chutes provider plugin API exports.
*/
export {
buildChutesModelDefinition,
CHUTES_BASE_URL,
CHUTES_DEFAULT_MODEL_ID,
CHUTES_DEFAULT_MODEL_REF,
CHUTES_MODEL_CATALOG,
discoverChutesModels,
} from "./models.js";
export { buildChutesProvider } from "./provider-catalog.js";
export {
applyChutesApiKeyConfig,
applyChutesConfig,
applyChutesProviderConfig,
} from "./onboard.js";

View File

@@ -0,0 +1,117 @@
// Chutes tests cover implicit provider plugin behavior.
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
import { resolveOAuthApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
import { afterEach, describe, expect, it, vi } from "vitest";
import plugin from "./index.js";
import { CHUTES_BASE_URL } from "./models.js";
const CHUTES_OAUTH_MARKER = resolveOAuthApiKeyMarker("chutes");
function restoreEnvVar(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
...init,
});
}
async function runChutesCatalog(params: { apiKey?: string; discoveryApiKey?: string }) {
const provider = await registerSingleProviderPlugin(plugin);
const result = await provider.catalog?.run({
config: {},
resolveProviderAuth: () => ({
apiKey: params.apiKey ?? "",
discoveryApiKey: params.discoveryApiKey,
}),
} as never);
return result ?? null;
}
async function runChutesCatalogProvider(params: { apiKey: string; discoveryApiKey?: string }) {
const result = await runChutesCatalog(params);
if (!result || !("provider" in result)) {
throw new Error("expected Chutes catalog to return one provider");
}
return result.provider;
}
async function withRealChutesDiscovery<T>(
run: (fetchMock: ReturnType<typeof vi.fn>) => Promise<T>,
) {
const originalVitest = process.env.VITEST;
const originalNodeEnv = process.env.NODE_ENV;
const originalFetch = globalThis.fetch;
delete process.env.VITEST;
delete process.env.NODE_ENV;
const fetchMock = vi
.fn()
.mockResolvedValue(jsonResponse({ data: [{ id: "chutes/private-model" }] }));
globalThis.fetch = fetchMock as unknown as typeof fetch;
try {
return await run(fetchMock);
} finally {
restoreEnvVar("VITEST", originalVitest);
restoreEnvVar("NODE_ENV", originalNodeEnv);
globalThis.fetch = originalFetch;
}
}
describe("chutes implicit provider auth mode", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("publishes the env vars used by core api-key auto-detection", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(provider.envVars).toEqual(["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"]);
});
it("does not publish a provider when no API key is resolved", async () => {
await expect(runChutesCatalog({})).resolves.toBeNull();
});
it("keeps api-key resolved Chutes profiles on the API-key loader path", async () => {
const provider = await runChutesCatalogProvider({ apiKey: "chutes-live-api-key" });
expect(provider.baseUrl).toBe(CHUTES_BASE_URL);
expect(provider.apiKey).toBe("chutes-live-api-key");
expect(provider.apiKey).not.toBe(CHUTES_OAUTH_MARKER);
});
it("uses the OAuth marker only for oauth-backed Chutes profiles", async () => {
const provider = await runChutesCatalogProvider({
apiKey: CHUTES_OAUTH_MARKER,
discoveryApiKey: "oauth-access-token",
});
expect(provider.baseUrl).toBe(CHUTES_BASE_URL);
expect(provider.apiKey).toBe(CHUTES_OAUTH_MARKER);
});
it("forwards oauth access token to Chutes model discovery", async () => {
await withRealChutesDiscovery(async (fetchMock) => {
await runChutesCatalogProvider({
apiKey: CHUTES_OAUTH_MARKER,
discoveryApiKey: "my-chutes-access-token",
});
const chutesCalls = fetchMock.mock.calls.filter(([url]) => String(url).includes("chutes.ai"));
expect(chutesCalls.length).toBeGreaterThan(0);
const request = chutesCalls[0]?.[1] as { headers?: HeadersInit } | undefined;
expect(new Headers(request?.headers).get("authorization")).toBe(
"Bearer my-chutes-access-token",
);
});
});
});

197
extensions/chutes/index.ts Normal file
View File

@@ -0,0 +1,197 @@
/**
* Chutes provider plugin entrypoint with OAuth and API-key auth methods.
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
resolveOAuthApiKeyMarker,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk/provider-auth";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import {
normalizeOptionalString,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { loginChutes } from "./oauth.js";
import {
CHUTES_DEFAULT_MODEL_REF,
applyChutesApiKeyConfig,
applyChutesProviderConfig,
} from "./onboard.js";
import { buildChutesProvider, buildStaticChutesProvider } from "./provider-catalog.js";
const PROVIDER_ID = "chutes";
async function runChutesOAuth(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
const isRemote = ctx.isRemote;
const redirectUri =
process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() || "http://127.0.0.1:1456/oauth-callback";
const scopes = process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke";
const clientId =
process.env.CHUTES_CLIENT_ID?.trim() ||
(
await ctx.prompter.text({
message: "Enter Chutes OAuth client id",
placeholder: "cid_xxx",
validate: (value: string) => (value?.trim() ? undefined : "Required"),
})
).trim();
const clientSecret = normalizeOptionalString(process.env.CHUTES_CLIENT_SECRET);
await ctx.prompter.note(
isRemote
? [
"You are running in a remote/VPS environment.",
"A URL will be shown for you to open in your LOCAL browser.",
"After signing in, paste the redirect URL back here.",
"",
`Redirect URI: ${redirectUri}`,
].join("\n")
: [
"Browser will open for Chutes authentication.",
"If the callback doesn't auto-complete, paste the redirect URL.",
"",
`Redirect URI: ${redirectUri}`,
].join("\n"),
"Chutes OAuth",
);
const progress = ctx.prompter.progress("Starting Chutes OAuth…");
try {
const { onAuth, onPrompt } = ctx.oauth.createVpsAwareHandlers({
isRemote,
prompter: ctx.prompter,
runtime: ctx.runtime,
spin: progress,
openUrl: ctx.openUrl,
localBrowserMessage: "Complete sign-in in browser…",
});
const creds = await loginChutes({
app: {
clientId,
clientSecret,
redirectUri,
scopes: scopes.split(/\s+/).filter(Boolean),
},
manual: isRemote,
onAuth,
onPrompt,
onProgress: (message) => progress.update(message),
});
progress.stop("Chutes OAuth complete");
return buildOauthProviderAuthResult({
providerId: PROVIDER_ID,
defaultModel: CHUTES_DEFAULT_MODEL_REF,
access: creds.access,
refresh: creds.refresh,
expires: creds.expires,
email: readStringValue(creds.email),
credentialExtra: {
clientId,
...("accountId" in creds && typeof creds.accountId === "string"
? { accountId: creds.accountId }
: {}),
},
configPatch: applyChutesProviderConfig({}),
notes: [
"Chutes OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.",
`Redirect URI: ${redirectUri}`,
],
});
} catch (err) {
progress.stop("Chutes OAuth failed");
await ctx.prompter.note(
[
"Trouble with OAuth?",
"Verify CHUTES_CLIENT_ID (and CHUTES_CLIENT_SECRET if required).",
`Verify the OAuth app redirect URI includes: ${redirectUri}`,
"Chutes docs: https://chutes.ai/docs/sign-in-with-chutes/overview",
].join("\n"),
"OAuth help",
);
throw err;
}
}
export default definePluginEntry({
id: PROVIDER_ID,
name: "Chutes Provider",
description: "Bundled Chutes.ai provider plugin",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Chutes",
docsPath: "/providers/chutes",
envVars: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
auth: [
{
id: "oauth",
label: "Chutes OAuth",
hint: "Browser sign-in",
kind: "oauth",
wizard: {
choiceId: "chutes",
choiceLabel: "Chutes (OAuth)",
choiceHint: "Browser sign-in",
groupId: "chutes",
groupLabel: "Chutes",
groupHint: "OAuth + API key",
},
run: async (ctx) => await runChutesOAuth(ctx),
},
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Chutes API key",
hint: "Open-source models including Llama, DeepSeek, and more",
optionKey: "chutesApiKey",
flagName: "--chutes-api-key",
envVar: "CHUTES_API_KEY",
promptMessage: "Enter Chutes API key",
noteTitle: "Chutes",
noteMessage: [
"Chutes provides access to leading open-source models including Llama, DeepSeek, and more.",
"Get your API key at: https://chutes.ai/settings/api-keys",
].join("\n"),
defaultModel: CHUTES_DEFAULT_MODEL_REF,
expectedProviders: ["chutes"],
applyConfig: (cfg) => applyChutesApiKeyConfig(cfg),
wizard: {
choiceId: "chutes-api-key",
choiceLabel: "Chutes API key",
groupId: "chutes",
groupLabel: "Chutes",
groupHint: "OAuth + API key",
},
}),
],
catalog: {
order: "profile",
run: async (ctx) => {
const { apiKey, discoveryApiKey } = ctx.resolveProviderAuth(PROVIDER_ID, {
oauthMarker: resolveOAuthApiKeyMarker(PROVIDER_ID),
});
if (!apiKey) {
return null;
}
return {
provider: {
...(await buildChutesProvider(discoveryApiKey)),
apiKey,
},
};
},
},
staticCatalog: {
order: "profile",
run: async () => ({
provider: buildStaticChutesProvider(),
}),
},
});
},
});

View File

@@ -0,0 +1,9 @@
/**
* Environment helper for Chutes model discovery behavior in tests.
*/
/** Returns whether dynamic Chutes model discovery should use test behavior. */
export function isChutesModelDiscoveryTestEnvironment(
env: Record<string, string | undefined> = process.env,
): boolean {
return env.NODE_ENV === "test" || env.VITEST === "true";
}

View File

@@ -0,0 +1,337 @@
// Chutes tests cover models plugin behavior.
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
buildChutesModelDefinition,
CHUTES_MODEL_CATALOG,
clearChutesModelCacheForTests,
discoverChutesModels,
} from "./models.js";
function restoreEnvVar(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
...init,
});
}
async function withLiveChutesDiscovery<T>(
fetchMock: ReturnType<typeof vi.fn>,
run: () => Promise<T>,
options?: { now?: string },
): Promise<T> {
const oldNodeEnv = process.env.NODE_ENV;
const oldVitest = process.env.VITEST;
delete process.env.NODE_ENV;
delete process.env.VITEST;
if (options?.now) {
vi.useFakeTimers();
vi.setSystemTime(new Date(options.now));
}
vi.stubGlobal("fetch", fetchMock);
try {
return await run();
} finally {
restoreEnvVar("NODE_ENV", oldNodeEnv);
restoreEnvVar("VITEST", oldVitest);
vi.unstubAllGlobals();
if (options?.now) {
vi.useRealTimers();
}
}
}
function createAuthEchoFetchMock() {
return vi.fn().mockImplementation((_url, init?: { headers?: HeadersInit }) => {
const auth = readAuthorizationHeader(init);
return Promise.resolve(
jsonResponse({
data: [{ id: auth ? `${auth}-model` : "public-model" }],
}),
);
});
}
function readAuthorizationHeader(init?: { headers?: HeadersInit }): string {
const headers = init?.headers;
if (headers instanceof Headers) {
return headers.get("Authorization") ?? "";
}
if (Array.isArray(headers)) {
return headers.find(([key]) => key.toLowerCase() === "authorization")?.[1] ?? "";
}
return headers?.Authorization ?? headers?.authorization ?? "";
}
function requireChutesModel(
models: Awaited<ReturnType<typeof discoverChutesModels>>,
index: number,
): Awaited<ReturnType<typeof discoverChutesModels>>[number] {
const model = models[index];
if (!model) {
throw new Error(`expected Chutes model at index ${index}`);
}
return model;
}
describe("chutes-models", () => {
beforeEach(() => {
clearChutesModelCacheForTests();
});
it("buildChutesModelDefinition returns config with required fields", () => {
const entry = CHUTES_MODEL_CATALOG[0];
const def = buildChutesModelDefinition(entry);
expect(def.id).toBe(entry.id);
expect(def.name).toBe(entry.name);
expect(def.reasoning).toBe(entry.reasoning);
expect(def.input).toEqual(entry.input);
expect(def.cost).toEqual(entry.cost);
expect(def.contextWindow).toBe(entry.contextWindow);
expect(def.maxTokens).toBe(entry.maxTokens);
if (!def.compat) {
throw new Error("expected Chutes model compat");
}
expect(def.compat.supportsUsageInStreaming).toBe(false);
});
it("keeps Qwen VL image limits in the runtime catalog", () => {
const visionModelIds = ["Qwen/Qwen2.5-VL-32B-Instruct", "Qwen/Qwen3-VL-235B-A22B-Instruct"];
for (const id of visionModelIds) {
const model = CHUTES_MODEL_CATALOG.find((candidate) => candidate.id === id);
expect(model).toBeDefined();
if (!model) {
throw new Error(`expected ${id}`);
}
expect(buildChutesModelDefinition(model).mediaInput).toEqual({
image: { maxPixels: 12845056, preferredSidePx: 2048, tokenMode: "provider" },
});
}
});
it("discoverChutesModels returns static catalog when accessToken is empty", async () => {
const models = await discoverChutesModels("");
expect(models).toHaveLength(CHUTES_MODEL_CATALOG.length);
expect(models.map((m) => m.id)).toEqual(CHUTES_MODEL_CATALOG.map((m) => m.id));
});
it("discoverChutesModels returns static catalog in test env by default", async () => {
const models = await discoverChutesModels("test-token");
expect(models).toHaveLength(CHUTES_MODEL_CATALOG.length);
expect(requireChutesModel(models, 0).id).toBe("Qwen/Qwen3-32B");
});
it("discoverChutesModels correctly maps API response when not in test env", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
{ id: "zai-org/GLM-4.7-TEE" },
{
id: "new-provider/new-model-r1",
supported_features: ["reasoning"],
input_modalities: ["text", "image"],
context_length: 200000,
max_output_length: 16384,
pricing: { prompt: 0.1, completion: 0.2 },
},
{ id: "new-provider/simple-model" },
],
}),
);
await withLiveChutesDiscovery(mockFetch, async () => {
const models = await discoverChutesModels("test-token-real-fetch");
expect(models.length).toBeGreaterThan(0);
if (models.length === 3) {
const firstModel = requireChutesModel(models, 0);
const secondModel = requireChutesModel(models, 1);
expect(firstModel.id).toBe("zai-org/GLM-4.7-TEE");
expect(secondModel.reasoning).toBe(true);
if (!secondModel.compat) {
throw new Error("expected Chutes API model compat");
}
expect(secondModel.compat.supportsUsageInStreaming).toBe(false);
}
});
});
it("falls back from malformed live token metadata", async () => {
const mockFetch = vi.fn().mockResolvedValue(
jsonResponse({
data: [
{
id: "provider/bad-window",
context_length: -1,
max_output_length: 16384.5,
},
{
id: "provider/bad-max-output",
context_length: Number.POSITIVE_INFINITY,
max_output_length: 0,
},
],
}),
);
await withLiveChutesDiscovery(mockFetch, async () => {
const models = await discoverChutesModels("malformed-token-metadata");
expect(requireChutesModel(models, 0)).toMatchObject({
id: "provider/bad-window",
contextWindow: 128000,
maxTokens: 4096,
});
expect(requireChutesModel(models, 1)).toMatchObject({
id: "provider/bad-max-output",
contextWindow: 128000,
maxTokens: 4096,
});
});
});
it("discoverChutesModels retries without auth on 401", async () => {
const mockFetch = vi.fn().mockImplementation((_url, init?: { headers?: HeadersInit }) => {
if (readAuthorizationHeader(init) === "Bearer test-token-error") {
return Promise.resolve(new Response("", { status: 401 }));
}
return Promise.resolve(
jsonResponse({
data: [
{
id: "Qwen/Qwen3-32B",
name: "Qwen/Qwen3-32B",
supported_features: ["reasoning"],
input_modalities: ["text"],
context_length: 40960,
max_output_length: 40960,
pricing: { prompt: 0.08, completion: 0.24 },
},
{
id: "unsloth/Mistral-Nemo-Instruct-2407",
name: "unsloth/Mistral-Nemo-Instruct-2407",
input_modalities: ["text"],
context_length: 131072,
max_output_length: 131072,
pricing: { prompt: 0.02, completion: 0.04 },
},
{
id: "deepseek-ai/DeepSeek-V3-0324-TEE",
name: "deepseek-ai/DeepSeek-V3-0324-TEE",
supported_features: ["reasoning"],
input_modalities: ["text"],
context_length: 131072,
max_output_length: 65536,
pricing: { prompt: 0.28, completion: 0.42 },
},
],
}),
);
});
await withLiveChutesDiscovery(mockFetch, async () => {
const models = await discoverChutesModels("test-token-error");
expect(models.length).toBeGreaterThan(0);
expect(mockFetch).toHaveBeenCalled();
});
});
it("does not cache fallback static catalog for non-OK responses", async () => {
const mockFetch = vi.fn().mockResolvedValue(new Response("", { status: 503 }));
await withLiveChutesDiscovery(mockFetch, async () => {
const first = await discoverChutesModels("chutes-fallback-token");
const second = await discoverChutesModels("chutes-fallback-token");
expect(first.map((m) => m.id)).toEqual(CHUTES_MODEL_CATALOG.map((m) => m.id));
expect(second.map((m) => m.id)).toEqual(CHUTES_MODEL_CATALOG.map((m) => m.id));
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
it("scopes discovery cache by access token", async () => {
const mockFetch = vi.fn().mockImplementation((_url, init?: { headers?: HeadersInit }) => {
const auth = readAuthorizationHeader(init);
if (auth === "Bearer chutes-token-a") {
return Promise.resolve(
jsonResponse({
data: [{ id: "private/model-a" }],
}),
);
}
if (auth === "Bearer chutes-token-b") {
return Promise.resolve(
jsonResponse({
data: [{ id: "private/model-b" }],
}),
);
}
return Promise.resolve(
jsonResponse({
data: [{ id: "public/model" }],
}),
);
});
await withLiveChutesDiscovery(mockFetch, async () => {
const modelsA = await discoverChutesModels("chutes-token-a");
const modelsB = await discoverChutesModels("chutes-token-b");
const modelsASecond = await discoverChutesModels("chutes-token-a");
expect(requireChutesModel(modelsA, 0).id).toBe("private/model-a");
expect(requireChutesModel(modelsB, 0).id).toBe("private/model-b");
expect(requireChutesModel(modelsASecond, 0).id).toBe("private/model-a");
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
it("evicts oldest token entries when cache reaches max size", async () => {
const mockFetch = createAuthEchoFetchMock();
await withLiveChutesDiscovery(mockFetch, async () => {
for (let i = 0; i < 150; i += 1) {
await discoverChutesModels(`cache-token-${i}`);
}
await discoverChutesModels("cache-token-0");
expect(mockFetch).toHaveBeenCalledTimes(151);
});
});
it("prunes expired token cache entries during subsequent discovery", async () => {
const mockFetch = createAuthEchoFetchMock();
await withLiveChutesDiscovery(
mockFetch,
async () => {
await discoverChutesModels("token-a");
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
await discoverChutesModels("token-b");
await discoverChutesModels("token-a");
expect(mockFetch).toHaveBeenCalledTimes(3);
},
{ now: "2026-03-01T00:00:00.000Z" },
);
});
it("does not cache 401 fallback under the failed token key", async () => {
const mockFetch = vi.fn().mockImplementation((_url, init?: { headers?: HeadersInit }) => {
if (readAuthorizationHeader(init) === "Bearer failed-token") {
return Promise.resolve(new Response("", { status: 401 }));
}
return Promise.resolve(
jsonResponse({
data: [{ id: "public/model" }],
}),
);
});
await withLiveChutesDiscovery(mockFetch, async () => {
await discoverChutesModels("failed-token");
await discoverChutesModels("failed-token");
expect(mockFetch).toHaveBeenCalledTimes(3);
});
});
});

590
extensions/chutes/models.ts Normal file
View File

@@ -0,0 +1,590 @@
/**
* Chutes model catalog, static model definitions, and dynamic model discovery.
*/
import {
clearLiveCatalogCacheForTests,
getCachedLiveProviderModelRows,
LiveModelCatalogHttpError,
} from "openclaw/plugin-sdk/provider-catalog-live-runtime";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
import {
asPositiveSafeInteger,
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { isChutesModelDiscoveryTestEnvironment } from "./model-discovery-env.js";
const log = createSubsystemLogger("chutes-models");
/** Base URL for Chutes OpenAI-compatible inference. */
export const CHUTES_BASE_URL = "https://llm.chutes.ai/v1";
/** Default Chutes model id used for onboarding. */
export const CHUTES_DEFAULT_MODEL_ID = "zai-org/GLM-4.7-TEE";
/** Default Chutes model ref used for onboarding. */
export const CHUTES_DEFAULT_MODEL_REF = `chutes/${CHUTES_DEFAULT_MODEL_ID}`;
const CHUTES_DEFAULT_CONTEXT_WINDOW = 128000;
const CHUTES_DEFAULT_MAX_TOKENS = 4096;
/** Bundled fallback Chutes model catalog. */
export const CHUTES_MODEL_CATALOG: ModelDefinitionConfig[] = [
{
id: "Qwen/Qwen3-32B",
name: "Qwen/Qwen3-32B",
reasoning: true,
input: ["text"],
contextWindow: 40960,
maxTokens: 40960,
cost: { input: 0.08, output: 0.24, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/Mistral-Nemo-Instruct-2407",
name: "unsloth/Mistral-Nemo-Instruct-2407",
reasoning: false,
input: ["text"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.02, output: 0.04, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-V3-0324-TEE",
name: "deepseek-ai/DeepSeek-V3-0324-TEE",
reasoning: true,
input: ["text"],
contextWindow: 163840,
maxTokens: 65536,
cost: { input: 0.25, output: 1, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE",
name: "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 65536,
cost: { input: 0.08, output: 0.55, cacheRead: 0, cacheWrite: 0 },
},
{
id: "openai/gpt-oss-120b-TEE",
name: "openai/gpt-oss-120b-TEE",
reasoning: true,
input: ["text"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 0.05, output: 0.45, cacheRead: 0, cacheWrite: 0 },
},
{
id: "chutesai/Mistral-Small-3.1-24B-Instruct-2503",
name: "chutesai/Mistral-Small-3.1-24B-Instruct-2503",
reasoning: false,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.03, output: 0.11, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-V3.2-TEE",
name: "deepseek-ai/DeepSeek-V3.2-TEE",
reasoning: true,
input: ["text"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 0.28, output: 0.42, cacheRead: 0, cacheWrite: 0 },
},
{
id: "zai-org/GLM-4.7-TEE",
name: "zai-org/GLM-4.7-TEE",
reasoning: true,
input: ["text"],
contextWindow: 202752,
maxTokens: 65535,
cost: { input: 0.4, output: 2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "moonshotai/Kimi-K2.5-TEE",
name: "moonshotai/Kimi-K2.5-TEE",
reasoning: true,
input: ["text", "image"],
contextWindow: 262144,
maxTokens: 65535,
cost: { input: 0.45, output: 2.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/gemma-3-27b-it",
name: "unsloth/gemma-3-27b-it",
reasoning: false,
input: ["text", "image"],
contextWindow: 128000,
maxTokens: 65536,
cost: { input: 0.04, output: 0.15, cacheRead: 0, cacheWrite: 0 },
},
{
id: "XiaomiMiMo/MiMo-V2-Flash-TEE",
name: "XiaomiMiMo/MiMo-V2-Flash-TEE",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 65536,
cost: { input: 0.09, output: 0.29, cacheRead: 0, cacheWrite: 0 },
},
{
id: "chutesai/Mistral-Small-3.2-24B-Instruct-2506",
name: "chutesai/Mistral-Small-3.2-24B-Instruct-2506",
reasoning: false,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.06, output: 0.18, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-R1-0528-TEE",
name: "deepseek-ai/DeepSeek-R1-0528-TEE",
reasoning: true,
input: ["text"],
contextWindow: 163840,
maxTokens: 65536,
cost: { input: 0.45, output: 2.15, cacheRead: 0, cacheWrite: 0 },
},
{
id: "zai-org/GLM-5-TEE",
name: "zai-org/GLM-5-TEE",
reasoning: true,
input: ["text"],
contextWindow: 202752,
maxTokens: 65535,
cost: { input: 0.95, output: 3.15, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-V3.1-TEE",
name: "deepseek-ai/DeepSeek-V3.1-TEE",
reasoning: true,
input: ["text"],
contextWindow: 163840,
maxTokens: 65536,
cost: { input: 0.2, output: 0.8, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-V3.1-Terminus-TEE",
name: "deepseek-ai/DeepSeek-V3.1-Terminus-TEE",
reasoning: true,
input: ["text"],
contextWindow: 163840,
maxTokens: 65536,
cost: { input: 0.23, output: 0.9, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/gemma-3-4b-it",
name: "unsloth/gemma-3-4b-it",
reasoning: false,
input: ["text", "image"],
contextWindow: 96000,
maxTokens: 96000,
cost: { input: 0.01, output: 0.03, cacheRead: 0, cacheWrite: 0 },
},
{
id: "MiniMaxAI/MiniMax-M2.5-TEE",
name: "MiniMaxAI/MiniMax-M2.5-TEE",
reasoning: true,
input: ["text"],
contextWindow: 196608,
maxTokens: 65536,
cost: { input: 0.3, output: 1.1, cacheRead: 0, cacheWrite: 0 },
},
{
id: "tngtech/DeepSeek-TNG-R1T2-Chimera",
name: "tngtech/DeepSeek-TNG-R1T2-Chimera",
reasoning: true,
input: ["text"],
contextWindow: 163840,
maxTokens: 163840,
cost: { input: 0.25, output: 0.85, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-Coder-Next-TEE",
name: "Qwen/Qwen3-Coder-Next-TEE",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 65536,
cost: { input: 0.12, output: 0.75, cacheRead: 0, cacheWrite: 0 },
},
{
id: "NousResearch/Hermes-4-405B-FP8-TEE",
name: "NousResearch/Hermes-4-405B-FP8-TEE",
reasoning: true,
input: ["text"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-V3",
name: "deepseek-ai/DeepSeek-V3",
reasoning: false,
input: ["text"],
contextWindow: 163840,
maxTokens: 163840,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "openai/gpt-oss-20b",
name: "openai/gpt-oss-20b",
reasoning: true,
input: ["text"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.04, output: 0.15, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/Llama-3.2-3B-Instruct",
name: "unsloth/Llama-3.2-3B-Instruct",
reasoning: false,
input: ["text"],
contextWindow: 128000,
maxTokens: 4096,
cost: { input: 0.01, output: 0.01, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/Mistral-Small-24B-Instruct-2501",
name: "unsloth/Mistral-Small-24B-Instruct-2501",
reasoning: false,
input: ["text", "image"],
contextWindow: 32768,
maxTokens: 32768,
cost: { input: 0.07, output: 0.3, cacheRead: 0, cacheWrite: 0 },
},
{
id: "zai-org/GLM-4.7-FP8",
name: "zai-org/GLM-4.7-FP8",
reasoning: true,
input: ["text"],
contextWindow: 202752,
maxTokens: 65535,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "zai-org/GLM-4.6-TEE",
name: "zai-org/GLM-4.6-TEE",
reasoning: true,
input: ["text"],
contextWindow: 202752,
maxTokens: 65536,
cost: { input: 0.4, output: 1.7, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3.5-397B-A17B-TEE",
name: "Qwen/Qwen3.5-397B-A17B-TEE",
reasoning: true,
input: ["text", "image"],
contextWindow: 262144,
maxTokens: 65536,
cost: { input: 0.55, output: 3.5, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen2.5-72B-Instruct",
name: "Qwen/Qwen2.5-72B-Instruct",
reasoning: false,
input: ["text"],
contextWindow: 32768,
maxTokens: 32768,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "NousResearch/DeepHermes-3-Mistral-24B-Preview",
name: "NousResearch/DeepHermes-3-Mistral-24B-Preview",
reasoning: false,
input: ["text"],
contextWindow: 32768,
maxTokens: 32768,
cost: { input: 0.02, output: 0.1, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-Next-80B-A3B-Instruct",
name: "Qwen/Qwen3-Next-80B-A3B-Instruct",
reasoning: false,
input: ["text"],
contextWindow: 262144,
maxTokens: 262144,
cost: { input: 0.1, output: 0.8, cacheRead: 0, cacheWrite: 0 },
},
{
id: "zai-org/GLM-4.6-FP8",
name: "zai-org/GLM-4.6-FP8",
reasoning: true,
input: ["text"],
contextWindow: 202752,
maxTokens: 65535,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-235B-A22B-Thinking-2507",
name: "Qwen/Qwen3-235B-A22B-Thinking-2507",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 262144,
cost: { input: 0.11, output: 0.6, cacheRead: 0, cacheWrite: 0 },
},
{
id: "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
name: "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
reasoning: true,
input: ["text"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.03, output: 0.11, cacheRead: 0, cacheWrite: 0 },
},
{
id: "tngtech/R1T2-Chimera-Speed",
name: "tngtech/R1T2-Chimera-Speed",
reasoning: true,
input: ["text"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 0.22, output: 0.6, cacheRead: 0, cacheWrite: 0 },
},
{
id: "zai-org/GLM-4.6V",
name: "zai-org/GLM-4.6V",
reasoning: true,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 65536,
cost: { input: 0.3, output: 0.9, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen2.5-VL-32B-Instruct",
name: "Qwen/Qwen2.5-VL-32B-Instruct",
reasoning: false,
input: ["text", "image"],
mediaInput: {
image: { maxPixels: 12845056, preferredSidePx: 2048, tokenMode: "provider" },
},
contextWindow: 16384,
maxTokens: 16384,
cost: { input: 0.05, output: 0.22, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-VL-235B-A22B-Instruct",
name: "Qwen/Qwen3-VL-235B-A22B-Instruct",
reasoning: false,
input: ["text", "image"],
mediaInput: {
image: { maxPixels: 12845056, preferredSidePx: 2048, tokenMode: "provider" },
},
contextWindow: 262144,
maxTokens: 262144,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-14B",
name: "Qwen/Qwen3-14B",
reasoning: true,
input: ["text"],
contextWindow: 40960,
maxTokens: 40960,
cost: { input: 0.05, output: 0.22, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen2.5-Coder-32B-Instruct",
name: "Qwen/Qwen2.5-Coder-32B-Instruct",
reasoning: false,
input: ["text"],
contextWindow: 32768,
maxTokens: 32768,
cost: { input: 0.03, output: 0.11, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3-30B-A3B",
name: "Qwen/Qwen3-30B-A3B",
reasoning: true,
input: ["text"],
contextWindow: 40960,
maxTokens: 40960,
cost: { input: 0.06, output: 0.22, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/gemma-3-12b-it",
name: "unsloth/gemma-3-12b-it",
reasoning: false,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.03, output: 0.1, cacheRead: 0, cacheWrite: 0 },
},
{
id: "unsloth/Llama-3.2-1B-Instruct",
name: "unsloth/Llama-3.2-1B-Instruct",
reasoning: false,
input: ["text"],
contextWindow: 128000,
maxTokens: 4096,
cost: { input: 0.01, output: 0.01, cacheRead: 0, cacheWrite: 0 },
},
{
id: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16-TEE",
name: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16-TEE",
reasoning: true,
input: ["text"],
contextWindow: 128000,
maxTokens: 4096,
cost: { input: 0.3, output: 1.2, cacheRead: 0, cacheWrite: 0 },
},
{
id: "NousResearch/Hermes-4-14B",
name: "NousResearch/Hermes-4-14B",
reasoning: true,
input: ["text"],
contextWindow: 40960,
maxTokens: 40960,
cost: { input: 0.01, output: 0.05, cacheRead: 0, cacheWrite: 0 },
},
{
id: "Qwen/Qwen3Guard-Gen-0.6B",
name: "Qwen/Qwen3Guard-Gen-0.6B",
reasoning: false,
input: ["text"],
contextWindow: 128000,
maxTokens: 4096,
cost: { input: 0.01, output: 0.01, cacheRead: 0, cacheWrite: 0 },
},
{
id: "rednote-hilab/dots.ocr",
name: "rednote-hilab/dots.ocr",
reasoning: false,
input: ["text", "image"],
contextWindow: 131072,
maxTokens: 131072,
cost: { input: 0.01, output: 0.01, cacheRead: 0, cacheWrite: 0 },
},
];
/** Adds Chutes provider compat metadata to one model catalog entry. */
export function buildChutesModelDefinition(
model: (typeof CHUTES_MODEL_CATALOG)[number],
): ModelDefinitionConfig {
return {
...model,
compat: {
supportsUsageInStreaming: false,
},
};
}
interface ChutesModelEntry {
id: string;
name?: string;
supported_features?: string[];
input_modalities?: string[];
context_length?: number;
max_output_length?: number;
pricing?: {
prompt?: number;
completion?: number;
};
[key: string]: unknown;
}
const CACHE_TTL = 5 * 60 * 1000;
/** Clears the dynamic Chutes model discovery cache for tests. */
export function clearChutesModelCacheForTests(): void {
clearLiveCatalogCacheForTests();
}
async function fetchChutesModelRows(accessToken?: string): Promise<readonly unknown[]> {
return await getCachedLiveProviderModelRows({
providerId: "chutes",
endpoint: `${CHUTES_BASE_URL}/models`,
discoveryApiKey: accessToken,
timeoutMs: 10_000,
ttlMs: CACHE_TTL,
buildRequestHeaders: ({ discoveryApiKey }) => ({
Accept: "application/json",
...(discoveryApiKey ? { Authorization: `Bearer ${discoveryApiKey}` } : {}),
}),
policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(CHUTES_BASE_URL),
auditContext: "chutes-model-discovery",
});
}
/** Discovers Chutes models dynamically, falling back to the bundled static catalog. */
export async function discoverChutesModels(accessToken?: string): Promise<ModelDefinitionConfig[]> {
const trimmedKey = normalizeOptionalString(accessToken) ?? "";
if (isChutesModelDiscoveryTestEnvironment()) {
return CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition);
}
const staticCatalog = () => CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition);
try {
const data = await fetchChutesModelRows(trimmedKey || undefined);
if (data.length === 0) {
log.warn("No models in response, using static catalog");
return staticCatalog();
}
const seen = new Set<string>();
const models: ModelDefinitionConfig[] = [];
for (const entry of data as ChutesModelEntry[]) {
const id = normalizeOptionalString(entry?.id) ?? "";
if (!id || seen.has(id)) {
continue;
}
seen.add(id);
const lowerId = normalizeLowercaseStringOrEmpty(id);
const isReasoning =
entry.supported_features?.includes("reasoning") ||
lowerId.includes("r1") ||
lowerId.includes("thinking") ||
lowerId.includes("reason") ||
lowerId.includes("tee");
const input: Array<"text" | "image"> = (entry.input_modalities || ["text"]).filter(
(i): i is "text" | "image" => i === "text" || i === "image",
);
models.push({
id,
name: id,
reasoning: isReasoning,
input,
cost: {
input: entry.pricing?.prompt || 0,
output: entry.pricing?.completion || 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: asPositiveSafeInteger(entry.context_length) ?? CHUTES_DEFAULT_CONTEXT_WINDOW,
maxTokens: asPositiveSafeInteger(entry.max_output_length) ?? CHUTES_DEFAULT_MAX_TOKENS,
compat: {
supportsUsageInStreaming: false,
},
});
}
if (models.length === 0) {
return staticCatalog();
}
return models;
} catch (error) {
if (error instanceof LiveModelCatalogHttpError && error.status === 401 && trimmedKey) {
return await discoverChutesModels(undefined);
}
if (
error instanceof LiveModelCatalogHttpError &&
error.status !== 401 &&
error.status !== 503
) {
log.warn(`GET /v1/models failed: HTTP ${error.status}, using static catalog`);
return staticCatalog();
}
log.warn(`Discovery failed: ${String(error)}, using static catalog`);
return staticCatalog();
}
}

12
extensions/chutes/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/chutes-provider",
"version": "2026.6.11"
}
}
}

View File

@@ -0,0 +1,178 @@
// Chutes tests cover oauth plugin behavior.
import { describe, expect, it, vi } from "vitest";
import { loginChutes } from "./oauth.js";
function boundedErrorResponse(
body: string,
status = 500,
): {
response: Response;
cancel: ReturnType<typeof vi.fn>;
releaseLock: ReturnType<typeof vi.fn>;
text: ReturnType<typeof vi.fn>;
} {
const encoded = new TextEncoder().encode(body);
let read = false;
const cancel = vi.fn(async () => undefined);
const releaseLock = vi.fn();
const text = vi.fn(async () => {
throw new Error("response.text() should not be called");
});
const response = {
ok: false,
status,
headers: new Headers(),
body: {
getReader: () => ({
read: async () => {
if (read) {
return { done: true, value: undefined };
}
read = true;
return { done: false, value: encoded };
},
cancel,
releaseLock,
}),
},
text,
} as unknown as Response;
return { response, cancel, releaseLock, text };
}
describe("chutes plugin OAuth", () => {
it("rejects unsafe token lifetimes before storing credentials", async () => {
const fetchFn = vi.fn(async (input: RequestInfo | URL) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url === "https://api.chutes.ai/idp/token") {
return new Response(
'{"access_token":"at_unsafe","refresh_token":"rt_unsafe","expires_in":1e309}',
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
return new Response("not found", { status: 404 });
});
await expect(
loginChutes({
app: {
clientId: "cid_test",
redirectUri: "http://127.0.0.1:1456/oauth-callback",
scopes: ["openid"],
},
manual: true,
createState: () => "state_test",
onAuth: vi.fn(async () => {}),
onPrompt: vi.fn(
async () => "http://127.0.0.1:1456/oauth-callback?code=code_test&state=state_test",
),
fetchFn,
}),
).rejects.toThrow("Chutes token exchange returned invalid expires_in");
});
it("bounds token exchange error bodies without requiring response.text()", async () => {
const errorResponse = boundedErrorResponse(
`${"chutes token unavailable ".repeat(1024)}tail-marker`,
502,
);
const fetchFn = vi.fn(async (input: RequestInfo | URL) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url === "https://api.chutes.ai/idp/token") {
return errorResponse.response;
}
return new Response("not found", { status: 404 });
});
let error: unknown;
try {
await loginChutes({
app: {
clientId: "cid_test",
redirectUri: "http://127.0.0.1:1456/oauth-callback",
scopes: ["openid"],
},
manual: true,
createState: () => "state_test",
onAuth: vi.fn(async () => {}),
onPrompt: vi.fn(
async () => "http://127.0.0.1:1456/oauth-callback?code=code_test&state=state_test",
),
fetchFn,
});
} catch (caught) {
error = caught;
}
expect(error).toBeInstanceOf(Error);
const message = (error as Error).message;
expect(message).toContain("Chutes token exchange failed: chutes token unavailable");
expect(message).not.toContain("tail-marker");
expect(errorResponse.text).not.toHaveBeenCalled();
expect(errorResponse.cancel).toHaveBeenCalledTimes(1);
expect(errorResponse.releaseLock).toHaveBeenCalledTimes(1);
});
it("cancels oversized token exchange JSON body via the 16 MiB provider cap", async () => {
const ONE_MIB = 1024 * 1024;
const TOTAL_CHUNKS = 32;
const chunk = new Uint8Array(ONE_MIB);
let bytesPulled = 0;
let canceled = false;
const oversizedTokenJson = new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
if (bytesPulled >= TOTAL_CHUNKS * ONE_MIB) {
controller.close();
return;
}
bytesPulled += chunk.length;
controller.enqueue(chunk);
},
cancel() {
canceled = true;
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
const fetchFn = vi.fn(async (input: RequestInfo | URL) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url === "https://api.chutes.ai/idp/userinfo") {
return new Response(JSON.stringify({ login: "test", name: "Test" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url === "https://api.chutes.ai/idp/token") {
return oversizedTokenJson;
}
return new Response("not found", { status: 404 });
});
await expect(
loginChutes({
app: {
clientId: "cid_test",
redirectUri: "http://127.0.0.1:1456/oauth-callback",
scopes: ["openid"],
},
manual: true,
createState: () => "state_test",
onAuth: vi.fn(async () => {}),
onPrompt: vi.fn(
async () => "http://127.0.0.1:1456/oauth-callback?code=code_test&state=state_test",
),
fetchFn,
}),
).rejects.toThrow(/Chutes token exchange: JSON response exceeds 16777216 bytes/);
expect(canceled).toBe(true);
expect(bytesPulled).toBeLessThan(TOTAL_CHUNKS * ONE_MIB);
});
});

262
extensions/chutes/oauth.ts Normal file
View File

@@ -0,0 +1,262 @@
/**
* Chutes OAuth PKCE login flow.
*/
import { randomBytes } from "node:crypto";
import { resolveExpiresAtMsFromDurationSeconds } from "openclaw/plugin-sdk/number-runtime";
import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/provider-auth";
import {
parseOAuthCallbackInput,
waitForLocalOAuthCallback,
} from "openclaw/plugin-sdk/provider-auth-runtime";
import {
readProviderJsonResponse,
readResponseTextLimited,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
const CHUTES_AUTHORIZE_ENDPOINT = "https://api.chutes.ai/idp/authorize";
const CHUTES_TOKEN_ENDPOINT = "https://api.chutes.ai/idp/token";
const CHUTES_USERINFO_ENDPOINT = "https://api.chutes.ai/idp/userinfo";
const CHUTES_TOKEN_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
type OAuthPrompt = {
message: string;
placeholder?: string;
};
type OAuthCredentials = {
refresh: string;
access: string;
expires: number;
[key: string]: unknown;
};
type ChutesOAuthAppConfig = {
clientId: string;
clientSecret?: string;
redirectUri: string;
scopes: string[];
};
type ChutesUserInfo = {
sub?: string;
username?: string;
};
type ChutesStoredOAuth = OAuthCredentials & {
accountId?: string;
clientId?: string;
};
function parseRedirectUri(redirectUri: string): {
hostname: string;
port: number;
pathname: string;
} {
const url = new URL(redirectUri);
if (url.protocol !== "http:") {
throw new Error(`Chutes OAuth redirect URI must be http:// (got ${redirectUri})`);
}
const hostname = url.hostname || "127.0.0.1";
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>/...`,
);
}
return {
hostname,
port: url.port ? Number.parseInt(url.port, 10) : 80,
pathname: url.pathname || "/",
};
}
function parseManualOAuthInput(
input: string,
expectedState: string,
): { code: string; state: string } {
const parsed = parseOAuthCallbackInput(input, {
invalidInput: "Paste the full redirect URL (must include code + state).",
missingState: "Missing 'state' parameter. Paste the full redirect URL.",
});
if ("error" in parsed) {
throw new Error(parsed.error);
}
if (parsed.state !== expectedState) {
throw new Error("OAuth state mismatch - possible CSRF attack. Please retry login.");
}
return parsed;
}
function buildAuthorizeUrl(params: {
clientId: string;
redirectUri: string;
scopes: string[];
state: string;
challenge: string;
}): string {
const qs = new URLSearchParams({
client_id: params.clientId,
redirect_uri: params.redirectUri,
response_type: "code",
scope: params.scopes.join(" "),
state: params.state,
code_challenge: params.challenge,
code_challenge_method: "S256",
});
return `${CHUTES_AUTHORIZE_ENDPOINT}?${qs.toString()}`;
}
function resolveChutesExpiresAt(value: unknown, now: number): number | undefined {
return resolveExpiresAtMsFromDurationSeconds(value, {
nowMs: now,
bufferMs: 5 * 60 * 1000,
minRemainingMs: 30_000,
});
}
async function fetchChutesUserInfo(params: {
accessToken: string;
fetchFn?: typeof fetch;
}): Promise<ChutesUserInfo | null> {
const fetchFn = params.fetchFn ?? fetch;
const response = await fetchFn(CHUTES_USERINFO_ENDPOINT, {
headers: { Authorization: `Bearer ${params.accessToken}` },
});
if (!response.ok) {
return null;
}
const data = await readProviderJsonResponse<unknown>(response, "Chutes userinfo");
return data && typeof data === "object" ? (data as ChutesUserInfo) : null;
}
async function exchangeChutesCodeForTokens(params: {
app: ChutesOAuthAppConfig;
code: string;
codeVerifier: string;
fetchFn?: typeof fetch;
now?: number;
}): Promise<ChutesStoredOAuth> {
const fetchFn = params.fetchFn ?? fetch;
const now = params.now ?? Date.now();
const body = new URLSearchParams(
toFormUrlEncoded({
grant_type: "authorization_code",
client_id: params.app.clientId,
code: params.code,
redirect_uri: params.app.redirectUri,
code_verifier: params.codeVerifier,
}),
);
if (params.app.clientSecret) {
body.set("client_secret", params.app.clientSecret);
}
const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!response.ok) {
const detail = await readResponseTextLimited(
response,
CHUTES_TOKEN_ERROR_BODY_LIMIT_BYTES,
).catch(() => "");
throw new Error(`Chutes token exchange failed: ${detail}`);
}
const data = await readProviderJsonResponse<{
access_token?: string;
refresh_token?: string;
expires_in?: number;
}>(response, "Chutes token exchange");
const access = normalizeOptionalString(data.access_token);
const refresh = normalizeOptionalString(data.refresh_token);
const expires = resolveChutesExpiresAt(data.expires_in, now);
if (!access) {
throw new Error("Chutes token exchange returned no access_token");
}
if (!refresh) {
throw new Error("Chutes token exchange returned no refresh_token");
}
if (expires === undefined) {
throw new Error("Chutes token exchange returned invalid expires_in");
}
const info = await fetchChutesUserInfo({ accessToken: access, fetchFn });
return {
access,
refresh,
expires,
email: info?.username,
accountId: info?.sub,
clientId: params.app.clientId,
} as ChutesStoredOAuth;
}
/** Runs Chutes OAuth and returns refreshable stored credentials. */
export async function loginChutes(params: {
app: ChutesOAuthAppConfig;
manual?: boolean;
timeoutMs?: number;
createState?: () => string;
onAuth: (event: { url: string }) => Promise<void>;
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
onProgress?: (message: string) => void;
fetchFn?: typeof fetch;
}): Promise<ChutesStoredOAuth> {
const { verifier, challenge } = generatePkceVerifierChallenge();
const state = params.createState?.() ?? randomBytes(16).toString("hex");
const timeoutMs = params.timeoutMs ?? 3 * 60 * 1000;
const url = buildAuthorizeUrl({
clientId: params.app.clientId,
redirectUri: params.app.redirectUri,
scopes: params.app.scopes,
state,
challenge,
});
let codeAndState: { code: string; state: string };
if (params.manual) {
await params.onAuth({ url });
params.onProgress?.("Waiting for redirect URL...");
codeAndState = parseManualOAuthInput(
await params.onPrompt({
message: "Paste the redirect URL",
placeholder: `${params.app.redirectUri}?code=...&state=...`,
}),
state,
);
} else {
const redirect = parseRedirectUri(params.app.redirectUri);
const callback = waitForLocalOAuthCallback({
expectedState: state,
timeoutMs,
port: redirect.port,
callbackPath: redirect.pathname,
redirectUri: params.app.redirectUri,
successTitle: "Chutes OAuth complete",
hostname: redirect.hostname,
onProgress: params.onProgress,
}).catch(async () => {
params.onProgress?.("OAuth callback not detected; paste redirect URL...");
return parseManualOAuthInput(
await params.onPrompt({
message: "Paste the redirect URL",
placeholder: `${params.app.redirectUri}?code=...&state=...`,
}),
state,
);
});
await params.onAuth({ url });
codeAndState = await callback;
}
params.onProgress?.("Exchanging code for tokens...");
return await exchangeChutesCodeForTokens({
app: params.app,
code: codeAndState.code,
codeVerifier: verifier,
fetchFn: params.fetchFn,
});
}

View File

@@ -0,0 +1,67 @@
/**
* Chutes onboarding config helpers for OAuth and API-key setup.
*/
import {
applyAgentDefaultModelPrimary,
applyProviderConfigWithModelCatalogPreset,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import {
CHUTES_BASE_URL,
CHUTES_DEFAULT_MODEL_REF,
CHUTES_MODEL_CATALOG,
buildChutesModelDefinition,
} from "./models.js";
export { CHUTES_DEFAULT_MODEL_REF };
/**
* Apply Chutes provider configuration without changing the default model.
* Registers all catalog models and sets provider aliases (chutes-fast, etc.).
*/
export function applyChutesProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
return applyProviderConfigWithModelCatalogPreset(cfg, {
providerId: "chutes",
api: "openai-completions",
baseUrl: CHUTES_BASE_URL,
catalogModels: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
aliases: [
...CHUTES_MODEL_CATALOG.map((model) => `chutes/${model.id}`),
{ modelRef: "chutes-fast", alias: "chutes/zai-org/GLM-4.7-FP8" },
{
modelRef: "chutes-vision",
alias: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506",
},
{ modelRef: "chutes-pro", alias: "chutes/deepseek-ai/DeepSeek-V3.2-TEE" },
],
});
}
/**
* Apply Chutes provider configuration AND set Chutes as the default model.
*/
export function applyChutesConfig(cfg: OpenClawConfig): OpenClawConfig {
const next = applyChutesProviderConfig(cfg);
return {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
model: {
primary: CHUTES_DEFAULT_MODEL_REF,
fallbacks: ["chutes/deepseek-ai/DeepSeek-V3.2-TEE", "chutes/Qwen/Qwen3-32B"],
},
imageModel: {
primary: "chutes/chutesai/Mistral-Small-3.2-24B-Instruct-2506",
fallbacks: ["chutes/chutesai/Mistral-Small-3.1-24B-Instruct-2503"],
},
},
},
};
}
/** Applies Chutes provider config and sets the default model for API-key auth. */
export function applyChutesApiKeyConfig(cfg: OpenClawConfig): OpenClawConfig {
return applyAgentDefaultModelPrimary(applyChutesProviderConfig(cfg), CHUTES_DEFAULT_MODEL_REF);
}

View File

@@ -0,0 +1,737 @@
{
"id": "chutes",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["chutes"],
"providerEndpoints": [
{
"endpointClass": "chutes-native",
"hosts": ["llm.chutes.ai"]
}
],
"providerRequest": {
"providers": {
"chutes": {
"family": "chutes"
}
}
},
"setup": {
"providers": [
{
"id": "chutes",
"envVars": ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"]
}
]
},
"providerAuthChoices": [
{
"provider": "chutes",
"method": "oauth",
"choiceId": "chutes",
"choiceLabel": "Chutes (OAuth)",
"choiceHint": "Browser sign-in",
"groupId": "chutes",
"groupLabel": "Chutes",
"groupHint": "OAuth + API key"
},
{
"provider": "chutes",
"method": "api-key",
"choiceId": "chutes-api-key",
"choiceLabel": "Chutes API key",
"choiceHint": "Open-source models including Llama, DeepSeek, and more",
"groupId": "chutes",
"groupLabel": "Chutes",
"groupHint": "OAuth + API key",
"optionKey": "chutesApiKey",
"cliFlag": "--chutes-api-key",
"cliOption": "--chutes-api-key <key>",
"cliDescription": "Chutes API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
},
"modelCatalog": {
"providers": {
"chutes": {
"baseUrl": "https://llm.chutes.ai/v1",
"api": "openai-completions",
"models": [
{
"id": "Qwen/Qwen3-32B",
"name": "Qwen/Qwen3-32B",
"reasoning": true,
"input": ["text"],
"contextWindow": 40960,
"maxTokens": 40960,
"cost": {
"input": 0.08,
"output": 0.24,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/Mistral-Nemo-Instruct-2407",
"name": "unsloth/Mistral-Nemo-Instruct-2407",
"reasoning": false,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.02,
"output": 0.04,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-V3-0324-TEE",
"name": "deepseek-ai/DeepSeek-V3-0324-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 65536,
"cost": {
"input": 0.25,
"output": 1,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE",
"name": "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 65536,
"cost": {
"input": 0.08,
"output": 0.55,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "openai/gpt-oss-120b-TEE",
"name": "openai/gpt-oss-120b-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 65536,
"cost": {
"input": 0.05,
"output": 0.45,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "chutesai/Mistral-Small-3.1-24B-Instruct-2503",
"name": "chutesai/Mistral-Small-3.1-24B-Instruct-2503",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.03,
"output": 0.11,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-V3.2-TEE",
"name": "deepseek-ai/DeepSeek-V3.2-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 65536,
"cost": {
"input": 0.28,
"output": 0.42,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "zai-org/GLM-4.7-TEE",
"name": "zai-org/GLM-4.7-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 202752,
"maxTokens": 65535,
"cost": {
"input": 0.4,
"output": 2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "moonshotai/Kimi-K2.5-TEE",
"name": "moonshotai/Kimi-K2.5-TEE",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 262144,
"maxTokens": 65535,
"cost": {
"input": 0.45,
"output": 2.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/gemma-3-27b-it",
"name": "unsloth/gemma-3-27b-it",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 128000,
"maxTokens": 65536,
"cost": {
"input": 0.04,
"output": 0.15,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "XiaomiMiMo/MiMo-V2-Flash-TEE",
"name": "XiaomiMiMo/MiMo-V2-Flash-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 65536,
"cost": {
"input": 0.09,
"output": 0.29,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "chutesai/Mistral-Small-3.2-24B-Instruct-2506",
"name": "chutesai/Mistral-Small-3.2-24B-Instruct-2506",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.06,
"output": 0.18,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-R1-0528-TEE",
"name": "deepseek-ai/DeepSeek-R1-0528-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 65536,
"cost": {
"input": 0.45,
"output": 2.15,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "zai-org/GLM-5-TEE",
"name": "zai-org/GLM-5-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 202752,
"maxTokens": 65535,
"cost": {
"input": 0.95,
"output": 3.15,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-V3.1-TEE",
"name": "deepseek-ai/DeepSeek-V3.1-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 65536,
"cost": {
"input": 0.2,
"output": 0.8,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-V3.1-Terminus-TEE",
"name": "deepseek-ai/DeepSeek-V3.1-Terminus-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 65536,
"cost": {
"input": 0.23,
"output": 0.9,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/gemma-3-4b-it",
"name": "unsloth/gemma-3-4b-it",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 96000,
"maxTokens": 96000,
"cost": {
"input": 0.01,
"output": 0.03,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "MiniMaxAI/MiniMax-M2.5-TEE",
"name": "MiniMaxAI/MiniMax-M2.5-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 196608,
"maxTokens": 65536,
"cost": {
"input": 0.3,
"output": 1.1,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "tngtech/DeepSeek-TNG-R1T2-Chimera",
"name": "tngtech/DeepSeek-TNG-R1T2-Chimera",
"reasoning": true,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 163840,
"cost": {
"input": 0.25,
"output": 0.85,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-Coder-Next-TEE",
"name": "Qwen/Qwen3-Coder-Next-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 65536,
"cost": {
"input": 0.12,
"output": 0.75,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "NousResearch/Hermes-4-405B-FP8-TEE",
"name": "NousResearch/Hermes-4-405B-FP8-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 65536,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-V3",
"name": "deepseek-ai/DeepSeek-V3",
"reasoning": false,
"input": ["text"],
"contextWindow": 163840,
"maxTokens": 163840,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "openai/gpt-oss-20b",
"name": "openai/gpt-oss-20b",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.04,
"output": 0.15,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/Llama-3.2-3B-Instruct",
"name": "unsloth/Llama-3.2-3B-Instruct",
"reasoning": false,
"input": ["text"],
"contextWindow": 128000,
"maxTokens": 4096,
"cost": {
"input": 0.01,
"output": 0.01,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/Mistral-Small-24B-Instruct-2501",
"name": "unsloth/Mistral-Small-24B-Instruct-2501",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 32768,
"maxTokens": 32768,
"cost": {
"input": 0.07,
"output": 0.3,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "zai-org/GLM-4.7-FP8",
"name": "zai-org/GLM-4.7-FP8",
"reasoning": true,
"input": ["text"],
"contextWindow": 202752,
"maxTokens": 65535,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "zai-org/GLM-4.6-TEE",
"name": "zai-org/GLM-4.6-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 202752,
"maxTokens": 65536,
"cost": {
"input": 0.4,
"output": 1.7,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3.5-397B-A17B-TEE",
"name": "Qwen/Qwen3.5-397B-A17B-TEE",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 262144,
"maxTokens": 65536,
"cost": {
"input": 0.55,
"output": 3.5,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen2.5-72B-Instruct",
"name": "Qwen/Qwen2.5-72B-Instruct",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
"maxTokens": 32768,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "NousResearch/DeepHermes-3-Mistral-24B-Preview",
"name": "NousResearch/DeepHermes-3-Mistral-24B-Preview",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
"maxTokens": 32768,
"cost": {
"input": 0.02,
"output": 0.1,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-Next-80B-A3B-Instruct",
"name": "Qwen/Qwen3-Next-80B-A3B-Instruct",
"reasoning": false,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 262144,
"cost": {
"input": 0.1,
"output": 0.8,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "zai-org/GLM-4.6-FP8",
"name": "zai-org/GLM-4.6-FP8",
"reasoning": true,
"input": ["text"],
"contextWindow": 202752,
"maxTokens": 65535,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-235B-A22B-Thinking-2507",
"name": "Qwen/Qwen3-235B-A22B-Thinking-2507",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 262144,
"cost": {
"input": 0.11,
"output": 0.6,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
"name": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.03,
"output": 0.11,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "tngtech/R1T2-Chimera-Speed",
"name": "tngtech/R1T2-Chimera-Speed",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 65536,
"cost": {
"input": 0.22,
"output": 0.6,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "zai-org/GLM-4.6V",
"name": "zai-org/GLM-4.6V",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 131072,
"maxTokens": 65536,
"cost": {
"input": 0.3,
"output": 0.9,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen2.5-VL-32B-Instruct",
"name": "Qwen/Qwen2.5-VL-32B-Instruct",
"reasoning": false,
"input": ["text", "image"],
"mediaInput": {
"image": { "maxPixels": 12845056, "preferredSidePx": 2048, "tokenMode": "provider" }
},
"contextWindow": 16384,
"maxTokens": 16384,
"cost": {
"input": 0.05,
"output": 0.22,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-VL-235B-A22B-Instruct",
"name": "Qwen/Qwen3-VL-235B-A22B-Instruct",
"reasoning": false,
"input": ["text", "image"],
"mediaInput": {
"image": { "maxPixels": 12845056, "preferredSidePx": 2048, "tokenMode": "provider" }
},
"contextWindow": 262144,
"maxTokens": 262144,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-14B",
"name": "Qwen/Qwen3-14B",
"reasoning": true,
"input": ["text"],
"contextWindow": 40960,
"maxTokens": 40960,
"cost": {
"input": 0.05,
"output": 0.22,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen2.5-Coder-32B-Instruct",
"name": "Qwen/Qwen2.5-Coder-32B-Instruct",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
"maxTokens": 32768,
"cost": {
"input": 0.03,
"output": 0.11,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3-30B-A3B",
"name": "Qwen/Qwen3-30B-A3B",
"reasoning": true,
"input": ["text"],
"contextWindow": 40960,
"maxTokens": 40960,
"cost": {
"input": 0.06,
"output": 0.22,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/gemma-3-12b-it",
"name": "unsloth/gemma-3-12b-it",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.03,
"output": 0.1,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "unsloth/Llama-3.2-1B-Instruct",
"name": "unsloth/Llama-3.2-1B-Instruct",
"reasoning": false,
"input": ["text"],
"contextWindow": 128000,
"maxTokens": 4096,
"cost": {
"input": 0.01,
"output": 0.01,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16-TEE",
"name": "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16-TEE",
"reasoning": true,
"input": ["text"],
"contextWindow": 128000,
"maxTokens": 4096,
"cost": {
"input": 0.3,
"output": 1.2,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "NousResearch/Hermes-4-14B",
"name": "NousResearch/Hermes-4-14B",
"reasoning": true,
"input": ["text"],
"contextWindow": 40960,
"maxTokens": 40960,
"cost": {
"input": 0.01,
"output": 0.05,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "Qwen/Qwen3Guard-Gen-0.6B",
"name": "Qwen/Qwen3Guard-Gen-0.6B",
"reasoning": false,
"input": ["text"],
"contextWindow": 128000,
"maxTokens": 4096,
"cost": {
"input": 0.01,
"output": 0.01,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "rednote-hilab/dots.ocr",
"name": "rednote-hilab/dots.ocr",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 131072,
"maxTokens": 131072,
"cost": {
"input": 0.01,
"output": 0.01,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
}
},
"discovery": {
"chutes": "refreshable"
}
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.6.11",
"description": "OpenClaw Chutes.ai provider plugin.",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/chutes-provider",
"npmSpec": "@openclaw/chutes-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.8"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* Chutes provider builders for static and dynamically discovered catalogs.
*/
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import {
CHUTES_BASE_URL,
CHUTES_MODEL_CATALOG,
buildChutesModelDefinition,
discoverChutesModels,
} from "./models.js";
/** Builds the static Chutes provider catalog from bundled model metadata. */
export function buildStaticChutesProvider(): ModelProviderConfig {
return {
baseUrl: CHUTES_BASE_URL,
api: "openai-completions",
models: CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
};
}
/**
* Build the Chutes provider with dynamic model discovery.
* Falls back to the static catalog on failure.
* Accepts an optional access token (API key or OAuth access token) for authenticated discovery.
*/
export async function buildChutesProvider(accessToken?: string): Promise<ModelProviderConfig> {
const models = await discoverChutesModels(accessToken);
return {
baseUrl: CHUTES_BASE_URL,
api: "openai-completions",
models: models.length > 0 ? models : CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
};
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}