mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix(google): avoid doubled media generation API version
Strip configured trailing /v1beta from Google music/video generation base URLs before calling the Google GenAI SDK.\n\nFixes #63240.\n\nThanks @Hybirdss.
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Google media generation: strip a configured trailing `/v1beta` from Google music/video provider base URLs before calling the Google GenAI SDK, preventing doubled `/v1beta/v1beta` paths. Fixes #63240. (#63258) Thanks @Hybirdss.
|
||||
- Google Chat: preserve reply text when a typing indicator message is deleted or can no longer be updated, so media captions and first text chunks are resent instead of silently disappearing. (#71498) Thanks @colin-lgtm.
|
||||
- Cron: tolerate malformed legacy job rows in startup, main-session system-event payloads, and human-readable `cron list` output so missing `state`, `payload.text`, or display fields no longer crash the scheduler or CLI. Fixes #66016, #65916, #64137, #57872, #59968, #63813, #52804, and #43163.
|
||||
- Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys.
|
||||
|
||||
@@ -82,6 +82,171 @@ describe("google music generation provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("strips /v1beta suffix from configured baseUrl before passing to GoogleGenAI SDK", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateContentMock.mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
inlineData: {
|
||||
data: Buffer.from("mp3-bytes").toString("base64"),
|
||||
mimeType: "audio/mpeg",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const provider = buildGoogleMusicGenerationProvider();
|
||||
await provider.generateMusic({
|
||||
provider: "google",
|
||||
model: "lyria-3-clip-preview",
|
||||
prompt: "ambient ocean",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
google: { baseUrl: "https://generativelanguage.googleapis.com/v1beta", models: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
instrumental: true,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT strip /v1beta when it appears mid-path (end-anchor proof)", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateContentMock.mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ inlineData: { data: Buffer.from("x").toString("base64"), mimeType: "audio/mpeg" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const provider = buildGoogleMusicGenerationProvider();
|
||||
await provider.generateMusic({
|
||||
provider: "google",
|
||||
model: "lyria-3-clip-preview",
|
||||
prompt: "test",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: { google: { baseUrl: "https://proxy.example.com/v1beta/route", models: [] } },
|
||||
},
|
||||
},
|
||||
instrumental: true,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: "https://proxy.example.com/v1beta/route",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes baseUrl unchanged when no /v1beta suffix is present", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateContentMock.mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ inlineData: { data: Buffer.from("x").toString("base64"), mimeType: "audio/mpeg" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const provider = buildGoogleMusicGenerationProvider();
|
||||
await provider.generateMusic({
|
||||
provider: "google",
|
||||
model: "lyria-3-clip-preview",
|
||||
prompt: "test",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
google: { baseUrl: "https://generativelanguage.googleapis.com", models: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
instrumental: true,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not set baseUrl when none is configured", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateContentMock.mockResolvedValue({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ inlineData: { data: Buffer.from("x").toString("base64"), mimeType: "audio/mpeg" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const provider = buildGoogleMusicGenerationProvider();
|
||||
await provider.generateMusic({
|
||||
provider: "google",
|
||||
model: "lyria-3-clip-preview",
|
||||
prompt: "test",
|
||||
cfg: {},
|
||||
instrumental: true,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.not.objectContaining({
|
||||
baseUrl: expect.anything(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects unsupported wav output on clip model", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/music-generation";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { normalizeGoogleApiBaseUrl } from "./api.js";
|
||||
import { resolveGoogleGenerativeAiApiOrigin } from "./api.js";
|
||||
import {
|
||||
createGoogleMusicGenerationProviderMetadata,
|
||||
DEFAULT_GOOGLE_MUSIC_MODEL,
|
||||
@@ -37,7 +37,7 @@ type GoogleGenerateMusicResponse = {
|
||||
|
||||
function resolveConfiguredGoogleMusicBaseUrl(req: MusicGenerationRequest): string | undefined {
|
||||
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
|
||||
return configured ? normalizeGoogleApiBaseUrl(configured) : undefined;
|
||||
return configured ? resolveGoogleGenerativeAiApiOrigin(configured) : undefined;
|
||||
}
|
||||
|
||||
function buildMusicPrompt(req: MusicGenerationRequest): string {
|
||||
|
||||
@@ -100,6 +100,121 @@ describe("google video generation provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("strips /v1beta suffix from configured baseUrl before passing to GoogleGenAI SDK", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateVideosMock.mockResolvedValue({
|
||||
done: true,
|
||||
response: {
|
||||
generatedVideos: [
|
||||
{ video: { videoBytes: Buffer.from("mp4").toString("base64"), mimeType: "video/mp4" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
await provider.generateVideo({
|
||||
provider: "google",
|
||||
model: "veo-3.1-fast-generate-preview",
|
||||
prompt: "A tiny robot watering a windowsill garden",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
google: { baseUrl: "https://generativelanguage.googleapis.com/v1beta", models: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
durationSeconds: 3,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT strip /v1beta when it appears mid-path (end-anchor proof)", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateVideosMock.mockResolvedValue({
|
||||
done: true,
|
||||
response: {
|
||||
generatedVideos: [
|
||||
{ video: { videoBytes: Buffer.from("mp4").toString("base64"), mimeType: "video/mp4" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
await provider.generateVideo({
|
||||
provider: "google",
|
||||
model: "veo-3.1-fast-generate-preview",
|
||||
prompt: "test",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: { google: { baseUrl: "https://proxy.example.com/v1beta/route", models: [] } },
|
||||
},
|
||||
},
|
||||
durationSeconds: 3,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: "https://proxy.example.com/v1beta/route",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes baseUrl unchanged when no /v1beta suffix is present", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateVideosMock.mockResolvedValue({
|
||||
done: true,
|
||||
response: {
|
||||
generatedVideos: [
|
||||
{ video: { videoBytes: Buffer.from("mp4").toString("base64"), mimeType: "video/mp4" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
await provider.generateVideo({
|
||||
provider: "google",
|
||||
model: "veo-3.1-fast-generate-preview",
|
||||
prompt: "test",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
google: { baseUrl: "https://generativelanguage.googleapis.com", models: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
durationSeconds: 3,
|
||||
});
|
||||
|
||||
expect(createGoogleGenAIMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects mixed image and video inputs", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
import { normalizeGoogleApiBaseUrl } from "./api.js";
|
||||
import { resolveGoogleGenerativeAiApiOrigin } from "./api.js";
|
||||
import {
|
||||
createGoogleVideoGenerationProviderMetadata,
|
||||
DEFAULT_GOOGLE_VIDEO_MODEL,
|
||||
@@ -29,7 +29,7 @@ const MAX_POLL_ATTEMPTS = 90;
|
||||
|
||||
function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined {
|
||||
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
|
||||
return configured ? normalizeGoogleApiBaseUrl(configured) : undefined;
|
||||
return configured ? resolveGoogleGenerativeAiApiOrigin(configured) : undefined;
|
||||
}
|
||||
|
||||
function parseVideoSize(size: string | undefined): { width: number; height: number } | undefined {
|
||||
|
||||
Reference in New Issue
Block a user