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
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:
12
extensions/chutes/README.md
Normal file
12
extensions/chutes/README.md
Normal 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
17
extensions/chutes/api.ts
Normal 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";
|
||||
117
extensions/chutes/implicit-provider.test.ts
Normal file
117
extensions/chutes/implicit-provider.test.ts
Normal 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
197
extensions/chutes/index.ts
Normal 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(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
9
extensions/chutes/model-discovery-env.ts
Normal file
9
extensions/chutes/model-discovery-env.ts
Normal 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";
|
||||
}
|
||||
337
extensions/chutes/models.test.ts
Normal file
337
extensions/chutes/models.test.ts
Normal 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
590
extensions/chutes/models.ts
Normal 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
12
extensions/chutes/npm-shrinkwrap.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
178
extensions/chutes/oauth.test.ts
Normal file
178
extensions/chutes/oauth.test.ts
Normal 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
262
extensions/chutes/oauth.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
67
extensions/chutes/onboard.ts
Normal file
67
extensions/chutes/onboard.ts
Normal 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);
|
||||
}
|
||||
737
extensions/chutes/openclaw.plugin.json
Normal file
737
extensions/chutes/openclaw.plugin.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
extensions/chutes/package.json
Normal file
35
extensions/chutes/package.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
33
extensions/chutes/provider-catalog.ts
Normal file
33
extensions/chutes/provider-catalog.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
16
extensions/chutes/tsconfig.json
Normal file
16
extensions/chutes/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user