Skip to content

Commit 2023e8c

Browse files
authored
Merge branch 'main' into policy-policy-conformance
2 parents 1b6ef87 + f212176 commit 2023e8c

9 files changed

Lines changed: 134 additions & 127 deletions

extensions/browser/src/browser/client-fetch.loopback-auth.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import "../test-support/browser-security.mock.js";
33
import type { OpenClawConfig } from "../config/config.js";
4+
import type { BrowserControlAuth } from "./control-auth.js";
45
import type { BrowserDispatchResponse } from "./routes/dispatcher.js";
56

7+
type BridgeAuth = NonNullable<
8+
ReturnType<typeof import("./bridge-auth-registry.js").getBridgeAuthForPort>
9+
>;
10+
611
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
712
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/ssrf-runtime")>(
813
"openclaw/plugin-sdk/ssrf-runtime",
@@ -36,11 +41,10 @@ const mocks = vi.hoisted(() => ({
3641
},
3742
},
3843
})),
39-
resolveBrowserControlAuth: vi.fn(() => ({
44+
resolveBrowserControlAuth: vi.fn<() => BrowserControlAuth>(() => ({
4045
token: "loopback-token",
41-
password: undefined,
4246
})),
43-
getBridgeAuthForPort: vi.fn(() => null),
47+
getBridgeAuthForPort: vi.fn<(port: number) => BridgeAuth | undefined>(() => undefined),
4448
startBrowserControlServiceFromConfig: vi.fn(async () => ({ ok: true })),
4549
dispatch: vi.fn(async (): Promise<BrowserDispatchResponse> => okDispatchResponse()),
4650
}));
@@ -143,9 +147,8 @@ describe("fetchBrowserJson loopback auth", () => {
143147
mocks.dispatch.mockReset().mockResolvedValue(okDispatchResponse());
144148
mocks.resolveBrowserControlAuth.mockReset().mockReturnValue({
145149
token: "loopback-token",
146-
password: undefined,
147150
});
148-
mocks.getBridgeAuthForPort.mockReset().mockReturnValue(null);
151+
mocks.getBridgeAuthForPort.mockReset().mockReturnValue(undefined);
149152
});
150153

151154
afterEach(() => {
@@ -209,10 +212,7 @@ describe("fetchBrowserJson loopback auth", () => {
209212
});
210213

211214
it("does not treat explicit port zero as the default loopback bridge port", async () => {
212-
mocks.resolveBrowserControlAuth.mockReturnValueOnce({
213-
token: undefined,
214-
password: undefined,
215-
});
215+
mocks.resolveBrowserControlAuth.mockReturnValueOnce({});
216216
mocks.getBridgeAuthForPort.mockReturnValueOnce({ token: "bridge-token" });
217217
const fetchMock = stubJsonFetchOk();
218218

extensions/fal/http-config.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { type AuthProfileStore, type OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
2+
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
3+
import {
4+
resolveProviderHttpRequestConfig,
5+
type ProviderRequestCapability,
6+
} from "openclaw/plugin-sdk/provider-http";
7+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
8+
9+
const DEFAULT_FAL_BASE_URL = "https://fal.run";
10+
11+
type FalAuthenticatedRequest = {
12+
cfg?: OpenClawConfig;
13+
agentDir?: string;
14+
authStore?: AuthProfileStore;
15+
};
16+
17+
function resolveFalConfiguredBaseUrl(cfg?: OpenClawConfig): string | undefined {
18+
return normalizeOptionalString(cfg?.models?.providers?.fal?.baseUrl);
19+
}
20+
21+
export async function resolveFalHttpRequestConfig(params: {
22+
req: FalAuthenticatedRequest;
23+
baseUrl?: string;
24+
capability: ProviderRequestCapability;
25+
}): Promise<ReturnType<typeof resolveProviderHttpRequestConfig>> {
26+
const auth = await resolveApiKeyForProvider({
27+
provider: "fal",
28+
cfg: params.req.cfg,
29+
agentDir: params.req.agentDir,
30+
store: params.req.authStore,
31+
});
32+
if (!auth.apiKey) {
33+
throw new Error("fal API key missing");
34+
}
35+
36+
return resolveProviderHttpRequestConfig({
37+
baseUrl: params.baseUrl ?? resolveFalConfiguredBaseUrl(params.req.cfg),
38+
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
39+
allowPrivateNetwork: false,
40+
defaultHeaders: {
41+
Authorization: `Key ${auth.apiKey}`,
42+
"Content-Type": "application/json",
43+
},
44+
provider: "fal",
45+
capability: params.capability,
46+
transport: "http",
47+
});
48+
}

extensions/fal/image-generation-provider.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import {
88
toImageDataUrl,
99
} from "openclaw/plugin-sdk/image-generation";
1010
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
11-
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
1211
import {
1312
assertOkOrThrowHttpError,
1413
assertOkOrThrowProviderError,
15-
resolveProviderHttpRequestConfig,
1614
} from "openclaw/plugin-sdk/provider-http";
1715
import {
1816
buildHostnameAllowlistPolicyFromSuffixAllowlist,
@@ -26,8 +24,8 @@ import {
2624
normalizeLowercaseStringOrEmpty,
2725
normalizeOptionalString,
2826
} from "openclaw/plugin-sdk/string-coerce-runtime";
27+
import { resolveFalHttpRequestConfig } from "./http-config.js";
2928

30-
const DEFAULT_FAL_BASE_URL = "https://fal.run";
3129
const DEFAULT_FAL_IMAGE_MODEL = "fal-ai/flux/dev";
3230
const DEFAULT_FAL_EDIT_SUBPATH = "image-to-image";
3331
const FAL_KREA_2_MODEL_PREFIX = "krea/v2/";
@@ -551,15 +549,6 @@ export function buildFalImageGenerationProvider(): ImageGenerationProvider {
551549
},
552550
},
553551
async generateImage(req) {
554-
const auth = await resolveApiKeyForProvider({
555-
provider: "fal",
556-
cfg: req.cfg,
557-
agentDir: req.agentDir,
558-
store: req.authStore,
559-
});
560-
if (!auth.apiKey) {
561-
throw new Error("fal API key missing");
562-
}
563552
const inputImageCount = req.inputImages?.length ?? 0;
564553
const hasInputImages = inputImageCount > 0;
565554
const requestedModel = req.model?.trim() || DEFAULT_FAL_IMAGE_MODEL;
@@ -588,20 +577,8 @@ export function buildFalImageGenerationProvider(): ImageGenerationProvider {
588577
if (!schema.supportsOutputFormat && req.outputFormat) {
589578
throw new Error(`fal ${requestedModel} does not support outputFormat overrides`);
590579
}
591-
const explicitBaseUrl = req.cfg?.models?.providers?.fal?.baseUrl?.trim();
592580
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
593-
resolveProviderHttpRequestConfig({
594-
baseUrl: explicitBaseUrl,
595-
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
596-
allowPrivateNetwork: false,
597-
defaultHeaders: {
598-
Authorization: `Key ${auth.apiKey}`,
599-
"Content-Type": "application/json",
600-
},
601-
provider: "fal",
602-
capability: "image",
603-
transport: "http",
604-
});
581+
await resolveFalHttpRequestConfig({ req, capability: "image" });
605582
const networkPolicy = resolveFalNetworkPolicy({ baseUrl, allowPrivateNetwork });
606583
const requestBody: Record<string, unknown> = {
607584
prompt: req.prompt,

extensions/fal/music-generation-provider.ts

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@ import {
55
type MusicGenerationRequest,
66
} from "openclaw/plugin-sdk/music-generation";
77
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
8-
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
9-
import {
10-
assertOkOrThrowHttpError,
11-
postJsonRequest,
12-
resolveProviderHttpRequestConfig,
13-
} from "openclaw/plugin-sdk/provider-http";
8+
import { assertOkOrThrowHttpError, postJsonRequest } from "openclaw/plugin-sdk/provider-http";
149
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
10+
import { resolveFalHttpRequestConfig } from "./http-config.js";
1511

16-
const DEFAULT_FAL_BASE_URL = "https://fal.run";
1712
const DEFAULT_FAL_MUSIC_MODEL = "fal-ai/minimax-music/v2.6";
1813
const FAL_ACE_STEP_MODEL = "fal-ai/ace-step/prompt-to-audio";
1914
const FAL_STABLE_AUDIO_MODEL = "fal-ai/stable-audio-25/text-to-audio";
@@ -29,10 +24,6 @@ function resolveFalMusicModel(model: string | undefined): string {
2924
return normalizeOptionalString(model) ?? DEFAULT_FAL_MUSIC_MODEL;
3025
}
3126

32-
function resolveFalMusicBaseUrl(req: MusicGenerationRequest): string | undefined {
33-
return normalizeOptionalString(req.cfg?.models?.providers?.fal?.baseUrl);
34-
}
35-
3627
function buildFalMinimaxBody(req: MusicGenerationRequest): Record<string, unknown> {
3728
const lyrics = normalizeOptionalString(req.lyrics);
3829
if (lyrics && req.instrumental === true) {
@@ -145,29 +136,8 @@ export function buildFalMusicGenerationProvider(): MusicGenerationProvider {
145136
throw new Error("fal music generation does not support image reference inputs.");
146137
}
147138

148-
const auth = await resolveApiKeyForProvider({
149-
provider: "fal",
150-
cfg: req.cfg,
151-
agentDir: req.agentDir,
152-
store: req.authStore,
153-
});
154-
if (!auth.apiKey) {
155-
throw new Error("fal API key missing");
156-
}
157-
158139
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
159-
resolveProviderHttpRequestConfig({
160-
baseUrl: resolveFalMusicBaseUrl(req),
161-
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
162-
allowPrivateNetwork: false,
163-
defaultHeaders: {
164-
Authorization: `Key ${auth.apiKey}`,
165-
"Content-Type": "application/json",
166-
},
167-
provider: "fal",
168-
capability: "audio",
169-
transport: "http",
170-
});
140+
await resolveFalHttpRequestConfig({ req, capability: "audio" });
171141
const model = resolveFalMusicModel(req.model);
172142
const { response, release } = await postJsonRequest({
173143
url: `${baseUrl}/${model}`,

extensions/fal/video-generation-provider.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
22
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
3-
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
4-
import {
5-
assertOkOrThrowHttpError,
6-
resolveProviderHttpRequestConfig,
7-
} from "openclaw/plugin-sdk/provider-http";
3+
import { assertOkOrThrowHttpError } from "openclaw/plugin-sdk/provider-http";
84
import {
95
fetchWithSsrFGuard,
106
type SsrFPolicy,
@@ -20,8 +16,8 @@ import type {
2016
VideoGenerationProvider,
2117
VideoGenerationRequest,
2218
} from "openclaw/plugin-sdk/video-generation";
19+
import { resolveFalHttpRequestConfig } from "./http-config.js";
2320

24-
const DEFAULT_FAL_BASE_URL = "https://fal.run";
2521
const DEFAULT_FAL_QUEUE_BASE_URL = "https://queue.fal.run";
2622
const DEFAULT_FAL_VIDEO_MODEL = "fal-ai/minimax/video-01-live";
2723
const HEYGEN_VIDEO_AGENT_MODEL = "fal-ai/heygen/v2/video-agent";
@@ -573,28 +569,8 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
573569
async generateVideo(req) {
574570
const model = normalizeOptionalString(req.model) || DEFAULT_FAL_VIDEO_MODEL;
575571
validateFalVideoReferenceInputs({ req, model });
576-
const auth = await resolveApiKeyForProvider({
577-
provider: "fal",
578-
cfg: req.cfg,
579-
agentDir: req.agentDir,
580-
store: req.authStore,
581-
});
582-
if (!auth.apiKey) {
583-
throw new Error("fal API key missing");
584-
}
585572
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
586-
resolveProviderHttpRequestConfig({
587-
baseUrl: normalizeOptionalString(req.cfg?.models?.providers?.fal?.baseUrl),
588-
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
589-
allowPrivateNetwork: false,
590-
defaultHeaders: {
591-
Authorization: `Key ${auth.apiKey}`,
592-
"Content-Type": "application/json",
593-
},
594-
provider: "fal",
595-
capability: "video",
596-
transport: "http",
597-
});
573+
await resolveFalHttpRequestConfig({ req, capability: "video" });
598574
const requestBody = buildFalVideoRequestBody({ req, model });
599575
const policy = buildPolicy(allowPrivateNetwork);
600576
const queueBaseUrl = resolveFalQueueBaseUrl(baseUrl);

src/agents/openai-transport-stream.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
import type { ModelCompatConfig } from "../config/types.models.js";
1616
import { getEnvApiKey } from "../llm/env-api-keys.js";
1717
import { calculateCost } from "../llm/model-utils.js";
18+
import { resolveAzureDeploymentNameFromMap } from "../llm/providers/azure-deployment-map.js";
1819
import { convertMessages } from "../llm/providers/openai-completions.js";
1920
import { clampOpenAIPromptCacheKey } from "../llm/providers/openai-prompt-cache.js";
2021
import type { Api, Context, Model } from "../llm/types.js";
@@ -2269,16 +2270,10 @@ function normalizeAzureBaseUrl(baseUrl: string): string {
22692270
}
22702271

22712272
function resolveAzureDeploymentName(model: Model): string {
2272-
const deploymentMap = process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
2273-
if (deploymentMap) {
2274-
for (const entry of deploymentMap.split(",")) {
2275-
const [modelId, deploymentName] = entry.split("=", 2).map((value) => value?.trim());
2276-
if (modelId === model.id && deploymentName) {
2277-
return deploymentName;
2278-
}
2279-
}
2280-
}
2281-
return model.id;
2273+
return resolveAzureDeploymentNameFromMap({
2274+
modelId: model.id,
2275+
deploymentMap: process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP,
2276+
});
22822277
}
22832278

22842279
function createAzureOpenAIClient(
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
parseAzureDeploymentNameMap,
4+
resolveAzureDeploymentNameFromMap,
5+
} from "./azure-deployment-map.js";
6+
7+
describe("Azure deployment name map", () => {
8+
it("preserves equals signs inside deployment names", () => {
9+
const map = parseAzureDeploymentNameMap("gpt-5=deployment=blue, ignored, gpt-4 = prod = east ");
10+
11+
expect(map.get("gpt-5")).toBe("deployment=blue");
12+
expect(map.get("gpt-4")).toBe("prod = east");
13+
expect(
14+
resolveAzureDeploymentNameFromMap({
15+
modelId: "gpt-5",
16+
deploymentMap: "gpt-5=deployment=blue",
17+
}),
18+
).toBe("deployment=blue");
19+
});
20+
21+
it("falls back to the model id when the map has no usable entry", () => {
22+
expect(
23+
resolveAzureDeploymentNameFromMap({
24+
modelId: "gpt-5",
25+
deploymentMap: "other=deployment,missing-value=",
26+
}),
27+
).toBe("gpt-5");
28+
});
29+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function parseAzureDeploymentNameMap(value: string | undefined): Map<string, string> {
2+
const map = new Map<string, string>();
3+
if (!value) {
4+
return map;
5+
}
6+
for (const entry of value.split(",")) {
7+
const trimmed = entry.trim();
8+
if (!trimmed) {
9+
continue;
10+
}
11+
const separator = trimmed.indexOf("=");
12+
if (separator <= 0) {
13+
continue;
14+
}
15+
const modelId = trimmed.slice(0, separator).trim();
16+
const deploymentName = trimmed.slice(separator + 1).trim();
17+
if (!modelId || !deploymentName) {
18+
continue;
19+
}
20+
map.set(modelId, deploymentName);
21+
}
22+
return map;
23+
}
24+
25+
export function resolveAzureDeploymentNameFromMap(params: {
26+
modelId: string;
27+
deploymentMap?: string;
28+
}): string {
29+
return parseAzureDeploymentNameMap(params.deploymentMap).get(params.modelId) || params.modelId;
30+
}

0 commit comments

Comments
 (0)