mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: discover self-hosted provider wildcards
This commit is contained in:
committed by
Sally O'Malley
parent
0c5bbdaad0
commit
3b361cf51c
@@ -20,7 +20,7 @@ SGLang serves open-weight models via an OpenAI-compatible HTTP API. OpenClaw con
|
||||
| Streaming usage | Yes (`supportsStreamingUsage: true`) |
|
||||
| Pricing | Marked external-free (`modelPricing.external: false`) |
|
||||
|
||||
OpenClaw also **auto-discovers** available models from SGLang when you opt in with `SGLANG_API_KEY` and you do not define an explicit `models.providers.sglang` entry — see [Model discovery (implicit provider)](#model-discovery-implicit-provider) below.
|
||||
OpenClaw also **auto-discovers** available models from SGLang when you opt in with `SGLANG_API_KEY`. Use `sglang/*` in `agents.defaults.models` to keep discovery dynamic when you also configure a custom SGLang base URL. See [Model discovery (implicit provider)](#model-discovery-implicit-provider) below.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -71,8 +71,10 @@ define `models.providers.sglang`, OpenClaw will query:
|
||||
and convert the returned IDs into model entries.
|
||||
|
||||
<Note>
|
||||
If you set `models.providers.sglang` explicitly, auto-discovery is skipped and
|
||||
you must define models manually.
|
||||
If you set `models.providers.sglang` explicitly, OpenClaw uses your declared
|
||||
models by default. Add `"sglang/*": {}` to `agents.defaults.models` when you
|
||||
want OpenClaw to query that configured provider's `/models` endpoint and include
|
||||
all advertised SGLang models.
|
||||
</Note>
|
||||
|
||||
## Explicit configuration (manual models)
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "vLLM"
|
||||
|
||||
vLLM can serve open-source (and some custom) models via an **OpenAI-compatible** HTTP API. OpenClaw connects to vLLM using the `openai-completions` API.
|
||||
|
||||
OpenClaw can also **auto-discover** available models from vLLM when you opt in with `VLLM_API_KEY` (any value works if your server does not enforce auth) and you do not define an explicit `models.providers.vllm` entry.
|
||||
OpenClaw can also **auto-discover** available models from vLLM when you opt in with `VLLM_API_KEY` (any value works if your server does not enforce auth). Use `vllm/*` in `agents.defaults.models` to keep discovery dynamic when you also configure a custom vLLM base URL.
|
||||
|
||||
OpenClaw treats `vllm` as a local OpenAI-compatible provider that supports
|
||||
streamed usage accounting, so status/context token counts can update from
|
||||
@@ -72,7 +72,7 @@ GET http://127.0.0.1:8000/v1/models
|
||||
and converts the returned IDs into model entries.
|
||||
|
||||
<Note>
|
||||
If you set `models.providers.vllm` explicitly, auto-discovery is skipped and you must define models manually.
|
||||
If you set `models.providers.vllm` explicitly, OpenClaw uses your declared models by default. Add `"vllm/*": {}` to `agents.defaults.models` when you want OpenClaw to query that configured provider's `/models` endpoint and include all advertised vLLM models.
|
||||
</Note>
|
||||
|
||||
## Explicit configuration (manual models)
|
||||
@@ -111,6 +111,21 @@ Use explicit config when:
|
||||
}
|
||||
```
|
||||
|
||||
To keep this provider dynamic without manually listing every model, add a provider
|
||||
wildcard to the visible model catalog:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vllm/*": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
<AccordionGroup>
|
||||
@@ -331,7 +346,7 @@ Use explicit config when:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="No models discovered">
|
||||
Auto-discovery requires `VLLM_API_KEY` to be set **and** no explicit `models.providers.vllm` config entry. If you have defined the provider manually, OpenClaw skips discovery and uses only your declared models.
|
||||
Auto-discovery requires `VLLM_API_KEY` to be set. If you have defined `models.providers.vllm`, OpenClaw uses only your declared models unless `agents.defaults.models` includes `"vllm/*": {}`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tools render as raw text">
|
||||
|
||||
@@ -1,128 +1,7 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describeVllmProviderDiscoveryContract } from "openclaw/plugin-sdk/provider-test-contracts";
|
||||
|
||||
const buildVllmProviderMock = vi.hoisted(() => vi.fn());
|
||||
type DiscoverOpenAICompatibleSelfHostedProviderParams = {
|
||||
buildProvider: (args: { apiKey?: string }) => Promise<Record<string, unknown>>;
|
||||
ctx: {
|
||||
resolveProviderApiKey: () => {
|
||||
apiKey?: string;
|
||||
};
|
||||
resolveProviderAuth: () => {
|
||||
discoveryApiKey?: string;
|
||||
};
|
||||
};
|
||||
providerId: string;
|
||||
};
|
||||
const discoverOpenAICompatibleSelfHostedProviderMock = vi.hoisted(() =>
|
||||
vi.fn(async (params: DiscoverOpenAICompatibleSelfHostedProviderParams) => ({
|
||||
provider: {
|
||||
...(await params.buildProvider({
|
||||
apiKey: params.ctx.resolveProviderAuth().discoveryApiKey,
|
||||
})),
|
||||
apiKey: params.ctx.resolveProviderApiKey().apiKey,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("./api.js", () => ({
|
||||
VLLM_DEFAULT_API_KEY_ENV_VAR: "VLLM_API_KEY",
|
||||
VLLM_DEFAULT_BASE_URL: "http://127.0.0.1:8000/v1",
|
||||
VLLM_MODEL_PLACEHOLDER: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
VLLM_PROVIDER_LABEL: "vLLM",
|
||||
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-setup", () => ({
|
||||
discoverOpenAICompatibleSelfHostedProvider: (
|
||||
params: DiscoverOpenAICompatibleSelfHostedProviderParams,
|
||||
) => discoverOpenAICompatibleSelfHostedProviderMock(params),
|
||||
}));
|
||||
|
||||
type ProviderDiscoveryRun = (ctx: {
|
||||
config: Record<string, unknown>;
|
||||
env: NodeJS.ProcessEnv;
|
||||
resolveProviderApiKey: () => {
|
||||
apiKey: string | undefined;
|
||||
discoveryApiKey?: string;
|
||||
};
|
||||
resolveProviderAuth: () => {
|
||||
apiKey: string | undefined;
|
||||
discoveryApiKey?: string;
|
||||
mode: "api_key" | "oauth" | "token" | "none";
|
||||
source: "env" | "profile" | "none";
|
||||
};
|
||||
}) => Promise<unknown>;
|
||||
|
||||
type RegisteredVllmProvider = {
|
||||
id: string;
|
||||
catalog?: {
|
||||
order?: string;
|
||||
run: ProviderDiscoveryRun;
|
||||
};
|
||||
};
|
||||
|
||||
describe("vllm provider discovery contract", () => {
|
||||
beforeEach(() => {
|
||||
buildVllmProviderMock.mockReset();
|
||||
discoverOpenAICompatibleSelfHostedProviderMock.mockClear();
|
||||
});
|
||||
|
||||
it("keeps self-hosted discovery provider-owned", async () => {
|
||||
const { default: plugin } = await import("./index.js");
|
||||
let provider: RegisteredVllmProvider | undefined;
|
||||
plugin.register({
|
||||
registerProvider: (registeredProvider) => {
|
||||
provider = registeredProvider as RegisteredVllmProvider;
|
||||
},
|
||||
} as OpenClawPluginApi);
|
||||
expect(provider?.id).toBe("vllm");
|
||||
expect(provider?.catalog?.order).toBe("late");
|
||||
const catalog = provider?.catalog;
|
||||
if (!catalog) {
|
||||
throw new Error("expected vllm provider catalog hook");
|
||||
}
|
||||
|
||||
buildVllmProviderMock.mockResolvedValueOnce({
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
api: "openai-completions",
|
||||
models: [{ id: "meta-llama/Meta-Llama-3-8B-Instruct", name: "Meta Llama 3" }],
|
||||
});
|
||||
|
||||
await expect(
|
||||
catalog.run({
|
||||
config: {},
|
||||
env: {
|
||||
VLLM_API_KEY: "env-vllm-key",
|
||||
} as NodeJS.ProcessEnv,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "VLLM_API_KEY",
|
||||
discoveryApiKey: "env-vllm-key",
|
||||
}),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "VLLM_API_KEY",
|
||||
discoveryApiKey: "env-vllm-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
provider: {
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "VLLM_API_KEY",
|
||||
models: [{ id: "meta-llama/Meta-Llama-3-8B-Instruct", name: "Meta Llama 3" }],
|
||||
},
|
||||
});
|
||||
expect(buildVllmProviderMock).toHaveBeenCalledWith({
|
||||
apiKey: "env-vllm-key",
|
||||
});
|
||||
expect(discoverOpenAICompatibleSelfHostedProviderMock).toHaveBeenCalledTimes(1);
|
||||
const [discoveryParams] = discoverOpenAICompatibleSelfHostedProviderMock.mock.calls.at(0) ?? [];
|
||||
if (discoveryParams === undefined) {
|
||||
throw new Error("expected discovery parameters");
|
||||
}
|
||||
expect(discoveryParams.providerId).toBe("vllm");
|
||||
expect(discoveryParams.buildProvider).toBeTypeOf("function");
|
||||
});
|
||||
describeVllmProviderDiscoveryContract({
|
||||
load: () => import("./index.js"),
|
||||
apiModuleId: fileURLToPath(new URL("./api.js", import.meta.url)),
|
||||
});
|
||||
|
||||
@@ -55,6 +55,18 @@ function createProvider(id: string): ProviderPlugin {
|
||||
};
|
||||
}
|
||||
|
||||
function createTextModel(id: string, name: string) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
reasoning: false,
|
||||
input: ["text" as const],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
}
|
||||
|
||||
function firstMockArg(mock: { mock: { calls: unknown[][] } }, label: string): unknown {
|
||||
const call = mock.mock.calls[0];
|
||||
if (!call) {
|
||||
@@ -121,4 +133,87 @@ describe("resolveImplicitProviders startup discovery scope", () => {
|
||||
) as { discoveryEntriesOnly?: boolean };
|
||||
expect(discoveryOptions?.discoveryEntriesOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps explicit provider models manual without provider wildcard visibility", async () => {
|
||||
const explicitProvider = {
|
||||
baseUrl: "http://vllm.example/v1",
|
||||
api: "openai-completions" as const,
|
||||
models: [createTextModel("manual-model", "Manual Model")],
|
||||
};
|
||||
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([createProvider("vllm")]);
|
||||
mocks.runProviderCatalog.mockResolvedValue({
|
||||
provider: {
|
||||
baseUrl: "http://vllm.example/v1",
|
||||
api: "openai-completions" as const,
|
||||
models: [createTextModel("discovered-model", "Discovered Model")],
|
||||
},
|
||||
});
|
||||
|
||||
const providers = await resolveImplicitProviders({
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vllm/manual-model": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
vllm: explicitProvider,
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
explicitProviders: {
|
||||
vllm: explicitProvider,
|
||||
},
|
||||
});
|
||||
|
||||
expect(providers?.vllm?.models.map((model) => model.id)).toEqual(["manual-model"]);
|
||||
});
|
||||
|
||||
it("merges discovered self-hosted models into explicit provider models for wildcard visibility", async () => {
|
||||
const explicitProvider = {
|
||||
baseUrl: "http://vllm.example/v1",
|
||||
api: "openai-completions" as const,
|
||||
models: [createTextModel("manual-model", "Manual Model")],
|
||||
};
|
||||
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([createProvider("vllm")]);
|
||||
mocks.runProviderCatalog.mockResolvedValue({
|
||||
provider: {
|
||||
baseUrl: "http://vllm.example/v1",
|
||||
api: "openai-completions" as const,
|
||||
models: [createTextModel("discovered-model", "Discovered Model")],
|
||||
},
|
||||
});
|
||||
|
||||
const providers = await resolveImplicitProviders({
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vllm/*": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
vllm: explicitProvider,
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
explicitProviders: {
|
||||
vllm: explicitProvider,
|
||||
},
|
||||
});
|
||||
|
||||
expect(providers?.vllm?.models.map((model) => model.id)).toEqual([
|
||||
"manual-model",
|
||||
"discovered-model",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
isNonSecretApiKeyMarker,
|
||||
resolveNonEnvSecretRefApiKeyMarker,
|
||||
} from "./model-auth-markers.js";
|
||||
import { parseConfiguredModelVisibilityEntries } from "./model-selection-shared.js";
|
||||
import { mergeProviderModels } from "./models-config.merge.js";
|
||||
import type {
|
||||
ProviderApiKeyResolver,
|
||||
ProviderAuthResolver,
|
||||
@@ -257,6 +259,7 @@ function mergeImplicitProviderConfig(params: {
|
||||
providerId: string;
|
||||
existing: ProviderConfig | undefined;
|
||||
implicit: ProviderConfig;
|
||||
dynamicProviderModels?: boolean;
|
||||
}): ProviderConfig {
|
||||
const { providerId, existing, implicit } = params;
|
||||
if (!existing) {
|
||||
@@ -266,6 +269,9 @@ function mergeImplicitProviderConfig(params: {
|
||||
if (merge) {
|
||||
return merge({ existing, implicit });
|
||||
}
|
||||
if (params.dynamicProviderModels) {
|
||||
return mergeProviderModels(implicit, existing);
|
||||
}
|
||||
return {
|
||||
...implicit,
|
||||
...existing,
|
||||
@@ -308,6 +314,15 @@ function resolveExistingImplicitProviderFromContext(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function hasProviderWildcardVisibility(params: {
|
||||
config?: OpenClawConfig;
|
||||
providerId: string;
|
||||
}): boolean {
|
||||
return parseConfiguredModelVisibilityEntries({ cfg: params.config }).providerWildcards.has(
|
||||
normalizeProviderId(params.providerId),
|
||||
);
|
||||
}
|
||||
|
||||
async function resolvePluginImplicitProviders(
|
||||
ctx: ImplicitProviderContext,
|
||||
providers: import("../plugins/types.js").ProviderPlugin[],
|
||||
@@ -388,6 +403,10 @@ async function resolvePluginImplicitProviders(
|
||||
],
|
||||
}),
|
||||
implicit: implicitProvider,
|
||||
dynamicProviderModels: hasProviderWildcardVisibility({
|
||||
config: ctx.config,
|
||||
providerId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ function runCatalog(
|
||||
provider: ProviderHandle;
|
||||
config?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
resolveProviderApiKey?: () => { apiKey: string | undefined };
|
||||
resolveProviderApiKey?: () => { apiKey: string | undefined; discoveryApiKey?: string };
|
||||
resolveProviderAuth?: (
|
||||
providerId?: string,
|
||||
options?: { oauthMarker?: string },
|
||||
@@ -376,6 +376,104 @@ export function describeVllmProviderDiscoveryContract(params: {
|
||||
apiKey: "env-vllm-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses configured transport only for provider wildcard discovery", async () => {
|
||||
buildVllmProviderMock.mockResolvedValueOnce({
|
||||
baseUrl: "http://vllm-router.example/v1",
|
||||
api: "openai-completions",
|
||||
models: [{ id: "router-model", name: "Router Model" }],
|
||||
});
|
||||
|
||||
await expect(
|
||||
runCatalog(state, {
|
||||
provider: state.vllmProvider!,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vllm/*": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
vllm: {
|
||||
baseUrl: "http://vllm-router.example/v1",
|
||||
apiKey: "VLLM_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {
|
||||
VLLM_API_KEY: "env-vllm-key",
|
||||
} as NodeJS.ProcessEnv,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "VLLM_API_KEY",
|
||||
discoveryApiKey: "env-vllm-key",
|
||||
}),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "VLLM_API_KEY",
|
||||
discoveryApiKey: "env-vllm-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
provider: {
|
||||
baseUrl: "http://vllm-router.example/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "VLLM_API_KEY",
|
||||
models: [{ id: "router-model", name: "Router Model" }],
|
||||
},
|
||||
});
|
||||
expect(buildVllmProviderMock).toHaveBeenCalledWith({
|
||||
apiKey: "env-vllm-key",
|
||||
baseUrl: "http://vllm-router.example/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit self-hosted provider config manual without wildcard visibility", async () => {
|
||||
await expect(
|
||||
runCatalog(state, {
|
||||
provider: state.vllmProvider!,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vllm/manual-model": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
vllm: {
|
||||
baseUrl: "http://vllm-router.example/v1",
|
||||
apiKey: "VLLM_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {
|
||||
VLLM_API_KEY: "env-vllm-key",
|
||||
} as NodeJS.ProcessEnv,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "VLLM_API_KEY",
|
||||
discoveryApiKey: "env-vllm-key",
|
||||
}),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "VLLM_API_KEY",
|
||||
discoveryApiKey: "env-vllm-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
expect(buildVllmProviderMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -429,6 +527,104 @@ export function describeSglangProviderDiscoveryContract(params: {
|
||||
apiKey: "env-sglang-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses configured transport only for provider wildcard discovery", async () => {
|
||||
buildSglangProviderMock.mockResolvedValueOnce({
|
||||
baseUrl: "http://sglang-router.example/v1",
|
||||
api: "openai-completions",
|
||||
models: [{ id: "Qwen/Qwen3-32B", name: "Qwen3-32B" }],
|
||||
});
|
||||
|
||||
await expect(
|
||||
runCatalog(state, {
|
||||
provider: state.sglangProvider!,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"sglang/*": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
sglang: {
|
||||
baseUrl: "http://sglang-router.example/v1",
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {
|
||||
SGLANG_API_KEY: "env-sglang-key",
|
||||
} as NodeJS.ProcessEnv,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
discoveryApiKey: "env-sglang-key",
|
||||
}),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
discoveryApiKey: "env-sglang-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
provider: {
|
||||
baseUrl: "http://sglang-router.example/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
models: [{ id: "Qwen/Qwen3-32B", name: "Qwen3-32B" }],
|
||||
},
|
||||
});
|
||||
expect(buildSglangProviderMock).toHaveBeenCalledWith({
|
||||
apiKey: "env-sglang-key",
|
||||
baseUrl: "http://sglang-router.example/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit self-hosted provider config manual without wildcard visibility", async () => {
|
||||
await expect(
|
||||
runCatalog(state, {
|
||||
provider: state.sglangProvider!,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"sglang/Qwen/Qwen3-32B": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
sglang: {
|
||||
baseUrl: "http://sglang-router.example/v1",
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
env: {
|
||||
SGLANG_API_KEY: "env-sglang-key",
|
||||
} as NodeJS.ProcessEnv,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
discoveryApiKey: "env-sglang-key",
|
||||
}),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
discoveryApiKey: "env-sglang-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
expect(buildSglangProviderMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ApiKeyCredential, AuthProfileCredential } from "../agents/auth-profiles/types.js";
|
||||
import { upsertAuthProfileWithLock } from "../agents/auth-profiles/upsert-with-lock.js";
|
||||
import { parseConfiguredModelVisibilityEntries } from "../agents/model-selection-shared.js";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "../agents/provider-id.js";
|
||||
import {
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
@@ -395,10 +397,23 @@ export async function discoverOpenAICompatibleSelfHostedProvider<
|
||||
>(params: {
|
||||
ctx: ProviderDiscoveryContext;
|
||||
providerId: string;
|
||||
buildProvider: (params: { apiKey?: string }) => Promise<T>;
|
||||
buildProvider: (params: { apiKey?: string; baseUrl?: string }) => Promise<T>;
|
||||
}): Promise<{ provider: T & { apiKey: string } } | null> {
|
||||
if (params.ctx.config.models?.providers?.[params.providerId]) {
|
||||
return null;
|
||||
const configuredProvider = findNormalizedProviderValue(
|
||||
params.ctx.config.models?.providers,
|
||||
params.providerId,
|
||||
);
|
||||
const configuredBaseUrl = configuredProvider
|
||||
? normalizeOptionalString(configuredProvider.baseUrl)
|
||||
: undefined;
|
||||
if (configuredProvider) {
|
||||
const visibility = parseConfiguredModelVisibilityEntries({ cfg: params.ctx.config });
|
||||
if (
|
||||
!visibility.providerWildcards.has(normalizeProviderId(params.providerId)) ||
|
||||
!configuredBaseUrl
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const { apiKey, discoveryApiKey } = params.ctx.resolveProviderApiKey(params.providerId);
|
||||
if (!apiKey) {
|
||||
@@ -406,7 +421,10 @@ export async function discoverOpenAICompatibleSelfHostedProvider<
|
||||
}
|
||||
return {
|
||||
provider: {
|
||||
...(await params.buildProvider({ apiKey: discoveryApiKey })),
|
||||
...(await params.buildProvider({
|
||||
apiKey: discoveryApiKey,
|
||||
...(configuredBaseUrl ? { baseUrl: configuredBaseUrl } : {}),
|
||||
})),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user