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:
9
extensions/litellm/api.ts
Normal file
9
extensions/litellm/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Litellm API module exposes the plugin public contract.
|
||||
export {
|
||||
applyLitellmConfig,
|
||||
applyLitellmProviderConfig,
|
||||
buildLitellmModelDefinition,
|
||||
LITELLM_BASE_URL,
|
||||
LITELLM_DEFAULT_MODEL_ID,
|
||||
LITELLM_DEFAULT_MODEL_REF,
|
||||
} from "./onboard.js";
|
||||
360
extensions/litellm/image-generation-provider.test.ts
Normal file
360
extensions/litellm/image-generation-provider.test.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
// Litellm tests cover image generation provider plugin behavior.
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildLitellmImageGenerationProvider } from "./image-generation-provider.js";
|
||||
|
||||
const {
|
||||
resolveApiKeyForProviderMock,
|
||||
postJsonRequestMock,
|
||||
postMultipartRequestMock,
|
||||
assertOkOrThrowHttpErrorMock,
|
||||
createProviderOperationDeadlineMock,
|
||||
resolveProviderHttpRequestConfigMock,
|
||||
resolveProviderOperationTimeoutMsMock,
|
||||
sanitizeConfiguredModelProviderRequestMock,
|
||||
} = vi.hoisted(() => ({
|
||||
resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "litellm-key" })),
|
||||
postJsonRequestMock: vi.fn(),
|
||||
postMultipartRequestMock: vi.fn(),
|
||||
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
|
||||
createProviderOperationDeadlineMock: vi.fn((params: Record<string, unknown>) => params),
|
||||
resolveProviderHttpRequestConfigMock: vi.fn((params) => ({
|
||||
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
|
||||
allowPrivateNetwork: Boolean(params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork),
|
||||
headers: new Headers(params.defaultHeaders),
|
||||
dispatcherPolicy: undefined as unknown,
|
||||
})),
|
||||
resolveProviderOperationTimeoutMsMock: vi.fn(
|
||||
(params: Record<string, unknown>) => params.defaultTimeoutMs,
|
||||
),
|
||||
sanitizeConfiguredModelProviderRequestMock: vi.fn((request) => request),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
|
||||
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-http", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-http")>(
|
||||
"openclaw/plugin-sdk/provider-http",
|
||||
);
|
||||
return {
|
||||
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
|
||||
createProviderOperationDeadline: createProviderOperationDeadlineMock,
|
||||
postJsonRequest: postJsonRequestMock,
|
||||
postMultipartRequest: postMultipartRequestMock,
|
||||
readProviderJsonResponse: actual.readProviderJsonResponse,
|
||||
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
|
||||
resolveProviderOperationTimeoutMs: resolveProviderOperationTimeoutMsMock,
|
||||
sanitizeConfiguredModelProviderRequest: sanitizeConfiguredModelProviderRequestMock,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.doUnmock("openclaw/plugin-sdk/provider-auth-runtime");
|
||||
vi.doUnmock("openclaw/plugin-sdk/provider-http");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
function jsonResponse(payload: unknown): Response {
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function mockGeneratedPngResponse() {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: jsonResponse({
|
||||
data: [{ b64_json: Buffer.from("png-bytes").toString("base64") }],
|
||||
}),
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
}
|
||||
|
||||
function mockObjectArg(mock: unknown, index = -1): Record<string, unknown> {
|
||||
const calls = (mock as { mock?: { calls?: Array<Array<unknown>> } }).mock?.calls ?? [];
|
||||
const call = index < 0 ? calls.at(index) : calls[index];
|
||||
const [arg] = call ?? [];
|
||||
if (!arg || typeof arg !== "object") {
|
||||
throw new Error(`expected mock object argument ${index}`);
|
||||
}
|
||||
return arg as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function expectFields(value: unknown, expected: Record<string, unknown>): void {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new Error("expected fields object");
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||
expect(record[key], key).toEqual(expectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
describe("litellm image generation provider", () => {
|
||||
afterEach(() => {
|
||||
resolveApiKeyForProviderMock.mockClear();
|
||||
postJsonRequestMock.mockReset();
|
||||
assertOkOrThrowHttpErrorMock.mockClear();
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
sanitizeConfiguredModelProviderRequestMock.mockClear();
|
||||
});
|
||||
|
||||
it("declares litellm id and OpenAI-compatible size hints", () => {
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
|
||||
expect(provider.id).toBe("litellm");
|
||||
expect(provider.label).toBe("LiteLLM");
|
||||
expect(provider.defaultModel).toBe("gpt-image-2");
|
||||
expect(provider.capabilities.geometry?.sizes).toContain("1024x1024");
|
||||
expect(provider.capabilities.geometry?.sizes).toContain("2048x2048");
|
||||
expect(provider.capabilities.geometry?.sizes).toContain("3840x2160");
|
||||
expect(provider.capabilities.edit?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to the loopback proxy and allows private network for localhost", async () => {
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "Draw a QA lighthouse",
|
||||
cfg: {},
|
||||
});
|
||||
|
||||
expectFields(mockObjectArg(resolveProviderHttpRequestConfigMock), {
|
||||
baseUrl: "http://localhost:4000",
|
||||
allowPrivateNetwork: true,
|
||||
});
|
||||
expectFields(mockObjectArg(postJsonRequestMock), {
|
||||
url: "http://localhost:4000/images/generations",
|
||||
allowPrivateNetwork: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("honors configured baseUrl and keeps private-network off for public endpoints", async () => {
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "campaign hero",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectFields(mockObjectArg(resolveProviderHttpRequestConfigMock), {
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
allowPrivateNetwork: undefined,
|
||||
});
|
||||
expectFields(mockObjectArg(postJsonRequestMock), {
|
||||
url: "https://proxy.example.com/v1/images/generations",
|
||||
allowPrivateNetwork: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards count and size overrides on generation requests", async () => {
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "dall-e-3",
|
||||
prompt: "two landscape variants",
|
||||
cfg: {},
|
||||
count: 2,
|
||||
size: "3840x2160",
|
||||
});
|
||||
|
||||
expectFields(mockObjectArg(postJsonRequestMock), {
|
||||
url: "http://localhost:4000/images/generations",
|
||||
body: {
|
||||
model: "dall-e-3",
|
||||
prompt: "two landscape variants",
|
||||
n: 2,
|
||||
size: "3840x2160",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("routes to the edit endpoint when input images are provided", async () => {
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "refine the hero",
|
||||
cfg: {},
|
||||
inputImages: [
|
||||
{
|
||||
buffer: Buffer.from("fake-input"),
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockObjectArg(postJsonRequestMock).url).toBe("http://localhost:4000/images/edits");
|
||||
const call = postJsonRequestMock.mock.calls[0]?.[0] as { body: { images: unknown[] } };
|
||||
expect(call.body.images).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("throws a clear error when the API key is missing", async () => {
|
||||
resolveApiKeyForProviderMock.mockResolvedValueOnce({ apiKey: "" });
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await expect(
|
||||
provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: {},
|
||||
}),
|
||||
).rejects.toThrow("LiteLLM API key missing");
|
||||
});
|
||||
|
||||
it("forwards dispatcherPolicy from resolveProviderHttpRequestConfig to postJsonRequest", async () => {
|
||||
const dispatcherPolicy = { proxyUrl: "http://corp-proxy:3128" } as unknown;
|
||||
resolveProviderHttpRequestConfigMock.mockReturnValueOnce({
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
allowPrivateNetwork: false,
|
||||
headers: new Headers({ Authorization: "Bearer litellm-key" }),
|
||||
dispatcherPolicy,
|
||||
});
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "hi",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
litellm: { baseUrl: "https://proxy.example.com/v1", models: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockObjectArg(postJsonRequestMock).dispatcherPolicy).toBe(dispatcherPolicy);
|
||||
});
|
||||
|
||||
it("auto-allows private network for loopback-style baseUrls", async () => {
|
||||
const cases = [
|
||||
"http://localhost:4000",
|
||||
"http://127.0.0.1:4000",
|
||||
"http://[::1]:4000",
|
||||
"http://host.docker.internal:4000",
|
||||
"https://localhost:4000",
|
||||
] as const;
|
||||
for (const baseUrl of cases) {
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
mockGeneratedPngResponse();
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: { models: { providers: { litellm: { baseUrl, models: [] } } } },
|
||||
});
|
||||
expect(
|
||||
mockObjectArg(resolveProviderHttpRequestConfigMock),
|
||||
`expected allowPrivateNetwork=true for ${baseUrl}`,
|
||||
).toHaveProperty("allowPrivateNetwork", true);
|
||||
}
|
||||
});
|
||||
|
||||
it("requires explicit private-network opt-in for LAN and internal baseUrls", async () => {
|
||||
const cases = [
|
||||
"http://10.0.0.42:4000",
|
||||
"http://192.168.5.10:4000",
|
||||
"http://172.16.0.5:4000",
|
||||
"https://192.168.5.10:4000",
|
||||
"http://printer.local:4000",
|
||||
"http://proxy.internal:4000",
|
||||
"https://metadata.google.internal",
|
||||
] as const;
|
||||
for (const baseUrl of cases) {
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
mockGeneratedPngResponse();
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: { models: { providers: { litellm: { baseUrl, models: [] } } } },
|
||||
});
|
||||
expect(
|
||||
mockObjectArg(resolveProviderHttpRequestConfigMock),
|
||||
`expected no automatic allowPrivateNetwork for ${baseUrl}`,
|
||||
).toHaveProperty("allowPrivateNetwork", undefined);
|
||||
expect(mockObjectArg(postJsonRequestMock).allowPrivateNetwork).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("honors explicit private-network opt-in for a LAN LiteLLM proxy", async () => {
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl: "http://192.168.5.10:4000",
|
||||
request: { allowPrivateNetwork: true },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectFields(mockObjectArg(resolveProviderHttpRequestConfigMock), {
|
||||
allowPrivateNetwork: undefined,
|
||||
request: { allowPrivateNetwork: true },
|
||||
});
|
||||
expect(mockObjectArg(postJsonRequestMock).allowPrivateNetwork).toBe(true);
|
||||
});
|
||||
|
||||
it("does not allow private network for public hosts that embed private strings in the URL", async () => {
|
||||
// Must not be fooled by an attacker-controlled URL that mentions
|
||||
// "host.docker.internal" (or any private-looking literal) in the path,
|
||||
// query string, or fragment. Only the parsed hostname should count.
|
||||
const cases = [
|
||||
"https://evil.example.com/?target=host.docker.internal",
|
||||
"https://evil.example.com/host.docker.internal/foo",
|
||||
"https://evil.example.com/redirect?to=127.0.0.1",
|
||||
"https://public-api.openai.com/v1",
|
||||
] as const;
|
||||
for (const baseUrl of cases) {
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
mockGeneratedPngResponse();
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: { models: { providers: { litellm: { baseUrl, models: [] } } } },
|
||||
});
|
||||
expect(
|
||||
mockObjectArg(resolveProviderHttpRequestConfigMock),
|
||||
`expected allowPrivateNetwork=false for ${baseUrl}`,
|
||||
).toHaveProperty("allowPrivateNetwork", undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
143
extensions/litellm/image-generation-provider.ts
Normal file
143
extensions/litellm/image-generation-provider.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
// Litellm provider module implements model/runtime integration.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
createOpenAiCompatibleImageGenerationProvider,
|
||||
type ImageGenerationProvider,
|
||||
type ImageGenerationSourceImage,
|
||||
toImageDataUrl,
|
||||
} from "openclaw/plugin-sdk/image-generation";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { LITELLM_BASE_URL } from "./onboard.js";
|
||||
|
||||
const DEFAULT_SIZE = "1024x1024";
|
||||
const DEFAULT_LITELLM_IMAGE_MODEL = "gpt-image-2";
|
||||
const LITELLM_SUPPORTED_SIZES = [
|
||||
"256x256",
|
||||
"512x512",
|
||||
"1024x1024",
|
||||
"1024x1536",
|
||||
"1024x1792",
|
||||
"1536x1024",
|
||||
"1792x1024",
|
||||
"2048x2048",
|
||||
"2048x1152",
|
||||
"3840x2160",
|
||||
"2160x3840",
|
||||
] as const;
|
||||
const LITELLM_MAX_INPUT_IMAGES = 5;
|
||||
|
||||
type LitellmProviderConfig = NonNullable<
|
||||
NonNullable<OpenClawConfig["models"]>["providers"]
|
||||
>[string];
|
||||
|
||||
function resolveLitellmProviderConfig(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
): LitellmProviderConfig | undefined {
|
||||
return cfg?.models?.providers?.litellm;
|
||||
}
|
||||
|
||||
function resolveConfiguredLitellmBaseUrl(cfg: OpenClawConfig | undefined): string {
|
||||
return normalizeOptionalString(resolveLitellmProviderConfig(cfg)?.baseUrl) ?? LITELLM_BASE_URL;
|
||||
}
|
||||
|
||||
function imageToDataUrl(image: ImageGenerationSourceImage): string {
|
||||
return toImageDataUrl({ buffer: image.buffer, mimeType: image.mimeType });
|
||||
}
|
||||
|
||||
// LiteLLM's default proxy is loopback. Auto-enable private-network access only
|
||||
// for loopback-style hosts; LAN/custom private endpoints should use the
|
||||
// explicit models.providers.litellm.request.allowPrivateNetwork opt-in.
|
||||
function isAutoAllowedLitellmHostname(hostname: string): boolean {
|
||||
if (!hostname) {
|
||||
return false;
|
||||
}
|
||||
// Strip IPv6 brackets if any: "[::1]" -> "::1".
|
||||
const host =
|
||||
hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
||||
const lowered = host.toLowerCase();
|
||||
if (
|
||||
lowered === "localhost" ||
|
||||
lowered === "host.docker.internal" ||
|
||||
lowered.endsWith(".localhost")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (lowered === "127.0.0.1" || lowered.startsWith("127.")) {
|
||||
return true;
|
||||
}
|
||||
if (lowered === "::1" || lowered === "0:0:0:0:0:0:0:1") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldAutoAllowPrivateLitellmEndpoint(baseUrl: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(baseUrl);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
return isAutoAllowedLitellmHostname(parsed.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildLitellmImageGenerationProvider(): ImageGenerationProvider {
|
||||
return createOpenAiCompatibleImageGenerationProvider({
|
||||
id: "litellm",
|
||||
label: "LiteLLM",
|
||||
defaultModel: DEFAULT_LITELLM_IMAGE_MODEL,
|
||||
models: [DEFAULT_LITELLM_IMAGE_MODEL],
|
||||
capabilities: {
|
||||
generate: {
|
||||
maxCount: 4,
|
||||
supportsSize: true,
|
||||
supportsAspectRatio: false,
|
||||
supportsResolution: false,
|
||||
},
|
||||
edit: {
|
||||
enabled: true,
|
||||
maxCount: 4,
|
||||
maxInputImages: LITELLM_MAX_INPUT_IMAGES,
|
||||
supportsSize: true,
|
||||
supportsAspectRatio: false,
|
||||
supportsResolution: false,
|
||||
},
|
||||
geometry: {
|
||||
sizes: [...LITELLM_SUPPORTED_SIZES],
|
||||
},
|
||||
},
|
||||
defaultBaseUrl: LITELLM_BASE_URL,
|
||||
resolveBaseUrl: ({ req }) => resolveConfiguredLitellmBaseUrl(req.cfg),
|
||||
resolveAllowPrivateNetwork: ({ baseUrl }) =>
|
||||
shouldAutoAllowPrivateLitellmEndpoint(baseUrl) ? true : undefined,
|
||||
useConfiguredRequest: true,
|
||||
buildGenerateRequest: ({ req, model, count }) => ({
|
||||
kind: "json",
|
||||
body: {
|
||||
model,
|
||||
prompt: req.prompt,
|
||||
n: count,
|
||||
size: req.size ?? DEFAULT_SIZE,
|
||||
},
|
||||
}),
|
||||
buildEditRequest: ({ req, inputImages, model, count }) => ({
|
||||
kind: "json",
|
||||
body: {
|
||||
model,
|
||||
prompt: req.prompt,
|
||||
n: count,
|
||||
size: req.size ?? DEFAULT_SIZE,
|
||||
images: inputImages.map((image) => ({
|
||||
image_url: imageToDataUrl(image),
|
||||
})),
|
||||
},
|
||||
}),
|
||||
missingApiKeyError: "LiteLLM API key missing",
|
||||
failureLabels: {
|
||||
generate: "LiteLLM image generation failed",
|
||||
edit: "LiteLLM image edit failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
108
extensions/litellm/index.test.ts
Normal file
108
extensions/litellm/index.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// Litellm tests cover index plugin behavior.
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { capturePluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
|
||||
const LITELLM_DEFAULT_MODEL = {
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
|
||||
function registerProvider() {
|
||||
const captured = capturePluginRegistration(plugin);
|
||||
const provider = captured.providers[0];
|
||||
expect(provider?.id).toBe("litellm");
|
||||
return provider;
|
||||
}
|
||||
|
||||
describe("litellm plugin", () => {
|
||||
it("honors --custom-base-url in non-interactive API-key setup", async () => {
|
||||
const provider = registerProvider();
|
||||
const auth = provider?.auth?.[0];
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-litellm-auth-"));
|
||||
const resolveApiKey = vi.fn(async () => ({ key: "litellm-test-key", source: "flag" as const }));
|
||||
const toApiKeyCredential = vi.fn(({ provider: providerId, resolved }) => ({
|
||||
type: "api_key" as const,
|
||||
provider: providerId,
|
||||
key: resolved.key,
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await auth?.runNonInteractive?.({
|
||||
authChoice: "litellm-api-key",
|
||||
config: {},
|
||||
baseConfig: {},
|
||||
opts: {
|
||||
litellmApiKey: "litellm-test-key",
|
||||
customBaseUrl: "https://litellm.example/v1/",
|
||||
},
|
||||
runtime: {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
log: vi.fn(),
|
||||
} as never,
|
||||
agentDir,
|
||||
resolveApiKey,
|
||||
toApiKeyCredential,
|
||||
} as never);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
auth: {
|
||||
profiles: {
|
||||
"litellm:default": {
|
||||
provider: "litellm",
|
||||
mode: "api_key",
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"litellm/claude-opus-4-6": {
|
||||
alias: "LiteLLM",
|
||||
},
|
||||
},
|
||||
model: {
|
||||
primary: "litellm/claude-opus-4-6",
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl: "https://litellm.example/v1",
|
||||
api: "openai-completions",
|
||||
models: [LITELLM_DEFAULT_MODEL],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(resolveApiKey).toHaveBeenCalledWith({
|
||||
provider: "litellm",
|
||||
flagValue: "litellm-test-key",
|
||||
flagName: "--litellm-api-key",
|
||||
envVar: "LITELLM_API_KEY",
|
||||
});
|
||||
expect(toApiKeyCredential).toHaveBeenCalledWith({
|
||||
provider: "litellm",
|
||||
resolved: { key: "litellm-test-key", source: "flag" },
|
||||
});
|
||||
} finally {
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
109
extensions/litellm/index.ts
Normal file
109
extensions/litellm/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// Litellm plugin entrypoint registers its OpenClaw integration.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
definePluginEntry,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
createProviderApiKeyAuthMethod,
|
||||
normalizeOptionalSecretInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import { buildLitellmImageGenerationProvider } from "./image-generation-provider.js";
|
||||
import { applyLitellmConfig, LITELLM_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import { buildLitellmProvider } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "litellm";
|
||||
|
||||
function applyCustomBaseUrlForNonInteractiveSetup(
|
||||
cfg: OpenClawConfig,
|
||||
customBaseUrl: unknown,
|
||||
): OpenClawConfig {
|
||||
const baseUrl = normalizeOptionalSecretInput(customBaseUrl)?.replace(/\/+$/, "");
|
||||
if (!baseUrl) {
|
||||
return cfg;
|
||||
}
|
||||
const existingProvider = cfg.models?.providers?.[PROVIDER_ID];
|
||||
return {
|
||||
...cfg,
|
||||
models: {
|
||||
...cfg.models,
|
||||
providers: {
|
||||
...cfg.models?.providers,
|
||||
[PROVIDER_ID]: {
|
||||
...existingProvider,
|
||||
baseUrl,
|
||||
models: existingProvider?.models ?? [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
name: "LiteLLM Provider",
|
||||
description: "Bundled LiteLLM provider plugin",
|
||||
register(api: OpenClawPluginApi) {
|
||||
const apiKeyAuth = createProviderApiKeyAuthMethod({
|
||||
providerId: PROVIDER_ID,
|
||||
methodId: "api-key",
|
||||
label: "LiteLLM API key",
|
||||
hint: "Unified gateway for 100+ LLM providers",
|
||||
optionKey: "litellmApiKey",
|
||||
flagName: "--litellm-api-key",
|
||||
envVar: "LITELLM_API_KEY",
|
||||
promptMessage: "Enter LiteLLM API key",
|
||||
defaultModel: LITELLM_DEFAULT_MODEL_REF,
|
||||
applyConfig: (cfg) => applyLitellmConfig(cfg),
|
||||
noteTitle: "LiteLLM",
|
||||
noteMessage: [
|
||||
"LiteLLM provides a unified API to 100+ LLM providers.",
|
||||
"Get your API key from your LiteLLM proxy or https://litellm.ai",
|
||||
"Default proxy runs on http://localhost:4000",
|
||||
].join("\n"),
|
||||
wizard: {
|
||||
choiceId: `${PROVIDER_ID}-api-key`,
|
||||
choiceLabel: "LiteLLM API key",
|
||||
groupId: PROVIDER_ID,
|
||||
groupLabel: "LiteLLM",
|
||||
groupHint: "Unified LLM gateway (100+ providers)",
|
||||
methodId: "api-key",
|
||||
},
|
||||
});
|
||||
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "LiteLLM",
|
||||
docsPath: "/providers/litellm",
|
||||
envVars: ["LITELLM_API_KEY"],
|
||||
auth: [
|
||||
{
|
||||
...apiKeyAuth,
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
||||
const runNonInteractive = apiKeyAuth.runNonInteractive;
|
||||
if (!runNonInteractive) {
|
||||
return null;
|
||||
}
|
||||
return await runNonInteractive({
|
||||
...ctx,
|
||||
config: applyCustomBaseUrlForNonInteractiveSetup(ctx.config, ctx.opts.customBaseUrl),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildLitellmProvider,
|
||||
allowExplicitBaseUrl: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
api.registerImageGenerationProvider(buildLitellmImageGenerationProvider());
|
||||
},
|
||||
});
|
||||
22
extensions/litellm/onboard.test.ts
Normal file
22
extensions/litellm/onboard.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Litellm tests cover onboard plugin behavior.
|
||||
import { expectProviderOnboardMergedLegacyConfig } from "openclaw/plugin-sdk/provider-test-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyLitellmProviderConfig } from "./onboard.js";
|
||||
|
||||
describe("litellm onboard", () => {
|
||||
it("preserves existing baseUrl and api key while adding the default model", () => {
|
||||
const provider = expectProviderOnboardMergedLegacyConfig({
|
||||
applyProviderConfig: applyLitellmProviderConfig,
|
||||
providerId: "litellm",
|
||||
providerApi: "openai-completions",
|
||||
baseUrl: "https://litellm.example/v1",
|
||||
legacyApi: "anthropic-messages",
|
||||
legacyModelId: "custom-model",
|
||||
legacyModelName: "Custom",
|
||||
legacyBaseUrl: "https://litellm.example/v1",
|
||||
legacyApiKey: " old-key ",
|
||||
});
|
||||
|
||||
expect(provider?.models.map((m) => m.id)).toEqual(["custom-model", "claude-opus-4-6"]);
|
||||
});
|
||||
});
|
||||
56
extensions/litellm/onboard.ts
Normal file
56
extensions/litellm/onboard.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// Litellm setup module handles plugin onboarding behavior.
|
||||
import {
|
||||
createDefaultModelPresetAppliers,
|
||||
type ModelDefinitionConfig,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/provider-onboard";
|
||||
|
||||
export const LITELLM_BASE_URL = "http://localhost:4000";
|
||||
export const LITELLM_DEFAULT_MODEL_ID = "claude-opus-4-6";
|
||||
export const LITELLM_DEFAULT_MODEL_REF = `litellm/${LITELLM_DEFAULT_MODEL_ID}`;
|
||||
const LITELLM_DEFAULT_CONTEXT_WINDOW = 1_000_000;
|
||||
const LITELLM_DEFAULT_MAX_TOKENS = 128_000;
|
||||
const LITELLM_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
export function buildLitellmModelDefinition(): ModelDefinitionConfig {
|
||||
return {
|
||||
id: LITELLM_DEFAULT_MODEL_ID,
|
||||
name: "Claude Opus 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: LITELLM_DEFAULT_COST,
|
||||
contextWindow: LITELLM_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: LITELLM_DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
const litellmPresetAppliers = createDefaultModelPresetAppliers({
|
||||
primaryModelRef: LITELLM_DEFAULT_MODEL_REF,
|
||||
resolveParams: (cfg: OpenClawConfig) => {
|
||||
const existingProvider = cfg.models?.providers?.litellm as { baseUrl?: unknown } | undefined;
|
||||
const resolvedBaseUrl =
|
||||
typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : "";
|
||||
|
||||
return {
|
||||
providerId: "litellm",
|
||||
api: "openai-completions" as const,
|
||||
baseUrl: resolvedBaseUrl || LITELLM_BASE_URL,
|
||||
defaultModel: buildLitellmModelDefinition(),
|
||||
defaultModelId: LITELLM_DEFAULT_MODEL_ID,
|
||||
aliases: [{ modelRef: LITELLM_DEFAULT_MODEL_REF, alias: "LiteLLM" }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export function applyLitellmProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return litellmPresetAppliers.applyProviderConfig(cfg);
|
||||
}
|
||||
|
||||
export function applyLitellmConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return litellmPresetAppliers.applyConfig(cfg);
|
||||
}
|
||||
40
extensions/litellm/openclaw.plugin.json
Normal file
40
extensions/litellm/openclaw.plugin.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "litellm",
|
||||
"activation": {
|
||||
"onStartup": false
|
||||
},
|
||||
"enabledByDefault": true,
|
||||
"providers": ["litellm"],
|
||||
"setup": {
|
||||
"providers": [
|
||||
{
|
||||
"id": "litellm",
|
||||
"envVars": ["LITELLM_API_KEY"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "litellm",
|
||||
"method": "api-key",
|
||||
"choiceId": "litellm-api-key",
|
||||
"choiceLabel": "LiteLLM API key",
|
||||
"choiceHint": "Unified gateway for 100+ LLM providers",
|
||||
"groupId": "litellm",
|
||||
"groupLabel": "LiteLLM",
|
||||
"groupHint": "Unified LLM gateway (100+ providers)",
|
||||
"optionKey": "litellmApiKey",
|
||||
"cliFlag": "--litellm-api-key",
|
||||
"cliOption": "--litellm-api-key <key>",
|
||||
"cliDescription": "LiteLLM API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"imageGenerationProviders": ["litellm"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
15
extensions/litellm/package.json
Normal file
15
extensions/litellm/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@openclaw/litellm-provider",
|
||||
"version": "2026.6.11",
|
||||
"private": true,
|
||||
"description": "OpenClaw LiteLLM provider plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
11
extensions/litellm/provider-catalog.ts
Normal file
11
extensions/litellm/provider-catalog.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Litellm provider module implements model/runtime integration.
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { buildLitellmModelDefinition, LITELLM_BASE_URL } from "./onboard.js";
|
||||
|
||||
export function buildLitellmProvider(): ModelProviderConfig {
|
||||
return {
|
||||
baseUrl: LITELLM_BASE_URL,
|
||||
api: "openai-completions",
|
||||
models: [buildLitellmModelDefinition()],
|
||||
};
|
||||
}
|
||||
16
extensions/litellm/tsconfig.json
Normal file
16
extensions/litellm/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