mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
The googleflow (Veo) video provider is live-confirmed broken on two independent axes: the submit/poll endpoints (/v1:generateVideo, /v1:fetchOperation) 404 on aisandbox-pa, and even the reporter's measured working endpoint (POST /v1/video:batchAsyncGenerateVideoText) rejects the stored Cloud Code OAuth bearer (401 UNAUTHENTICATED) since the cclog/cloud-platform scopes do not grant aisandbox-pa. Only a headed-browser reCAPTCHA session works (confirmed against gflow-cli's own docs), which cannot run headlessly. Exclude googleflow from getAllVideoModels() so it stops being advertised in /v1/models, and make handleGoogleFlowVideoGeneration fail fast with a clear diagnostic instead of forwarding to the known-wrong path and surfacing a raw HTML 404. Co-authored-by: Markus Hartung <mail@hartmark.se>
This commit is contained in:
committed by
GitHub
parent
cc544db38b
commit
e1c2425ed7
@@ -0,0 +1 @@
|
||||
- fix(video): stop advertising the googleflow (Veo) video provider as working and fail fast with a clear diagnostic — its submit/poll endpoints 404 and no server-side OAuth transport can satisfy the working endpoint (#10285)
|
||||
@@ -27,6 +27,12 @@ interface VideoProvider {
|
||||
authHeader: string;
|
||||
format: string;
|
||||
models: VideoModel[];
|
||||
// #10285 — set when a provider is registered (so parseVideoModel/getVideoProvider
|
||||
// still resolve it for a clear diagnostic) but must NOT be advertised as a working
|
||||
// model in /v1/models or getAllVideoModels(). Keep unsupportedReason short and
|
||||
// stable — handlers may surface it verbatim in the fail-fast error message.
|
||||
unsupported?: boolean;
|
||||
unsupportedReason?: string;
|
||||
}
|
||||
|
||||
export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
|
||||
@@ -119,6 +125,18 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
|
||||
{ id: "veo-3.1-fast-generate", name: "Veo 3.1 Fast (Google Flow)" },
|
||||
{ id: "veo-3.0-generate", name: "Veo 3.0 (Google Flow)" },
|
||||
],
|
||||
// #10285 — live-validated: the submit/poll paths above (/v1:generateVideo,
|
||||
// /v1:fetchOperation) are 404 on aisandbox-pa; the reporter's measured working
|
||||
// path (POST /v1/video:batchAsyncGenerateVideoText) is undocumented and, even
|
||||
// reached, rejects the stored Cloud Code OAuth bearer (401 UNAUTHENTICATED —
|
||||
// the cclog/cloud-platform scopes do not grant aisandbox-pa). gflow-cli confirms
|
||||
// only a headed-browser reCAPTCHA session works for mutation endpoints. De-listed
|
||||
// until a viable server-side transport is confirmed live (see plan-file #10285).
|
||||
unsupported: true,
|
||||
unsupportedReason:
|
||||
"Google Flow video generation requires a browser-session transport " +
|
||||
"(Flow/Cloud Code session with reCAPTCHA) and is not supported over the stored " +
|
||||
"OAuth bearer. Generate video via labs.google/flow directly for now.",
|
||||
},
|
||||
|
||||
kie: {
|
||||
@@ -399,17 +417,19 @@ export function parseVideoModel(modelStr: string | null) {
|
||||
* Get all video models as a flat list
|
||||
*/
|
||||
export function getAllVideoModels() {
|
||||
return Object.entries(VIDEO_PROVIDERS).flatMap(([providerId, config]) =>
|
||||
[providerId, config.alias]
|
||||
.filter((prefix): prefix is string => Boolean(prefix))
|
||||
.flatMap((prefix) =>
|
||||
config.models.map((model) => ({
|
||||
id: `${prefix}/${model.id}`,
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
supportedSizes: model.supportedSizes || [],
|
||||
mediaCapabilities: model.mediaCapabilities,
|
||||
}))
|
||||
)
|
||||
);
|
||||
return Object.entries(VIDEO_PROVIDERS)
|
||||
.filter(([, config]) => !config.unsupported)
|
||||
.flatMap(([providerId, config]) =>
|
||||
[providerId, config.alias]
|
||||
.filter((prefix): prefix is string => Boolean(prefix))
|
||||
.flatMap((prefix) =>
|
||||
config.models.map((model) => ({
|
||||
id: `${prefix}/${model.id}`,
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
supportedSizes: model.supportedSizes || [],
|
||||
mediaCapabilities: model.mediaCapabilities,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
/**
|
||||
* Veo video generation via Google Flow (labs.google/flow) — request orchestration.
|
||||
*
|
||||
* Uses the Google account OAuth bearer + Cloud Code projectId that the Antigravity
|
||||
* provider already establishes — no separate OAuth flow is added. Submits the
|
||||
* documented Veo `predictLongRunning` body to Google's AI Sandbox endpoint, polls
|
||||
* the long-running operation, and returns the MP4 (base64 or URL).
|
||||
*
|
||||
* ⚠️ PENDING LIVE VALIDATION (Hard Rule #18): the AI-Sandbox host/path and the
|
||||
* Cloud-Code request envelope cannot be unit-tested — they require a real Google
|
||||
* Flow account + a captured HAR. The wire surface is isolated to the two path
|
||||
* constants (`GOOGLE_FLOW_SUBMIT_PATH`/`GOOGLE_FLOW_POLL_PATH`) and the `project`
|
||||
* wrap below, so confirming a captured HAR is a one-line change. The pure
|
||||
* transformation helpers are fully unit-tested (google-flow-video-4569.test.ts).
|
||||
* ⚠️ #10285 — DISABLED pending a viable transport (Hard Rule #18). Live probes
|
||||
* against https://aisandbox-pa.googleapis.com confirmed two independent wire-surface
|
||||
* defects the #4769 PENDING LIVE VALIDATION flag anticipated: (1) the submit/poll
|
||||
* paths in googleFlow.ts (`GOOGLE_FLOW_SUBMIT_PATH`/`GOOGLE_FLOW_POLL_PATH`) 404 —
|
||||
* the real working endpoint is undocumented (`POST /v1/video:batchAsyncGenerateVideoText`);
|
||||
* (2) even on that working endpoint, the stored Cloud Code OAuth bearer is rejected
|
||||
* (401 UNAUTHENTICATED — the cclog/cloud-platform scopes do not grant aisandbox-pa).
|
||||
* gflow-cli's own docs confirm only a headed-browser reCAPTCHA session works for
|
||||
* mutation endpoints, which cannot run headlessly. Until a viable server-side
|
||||
* transport is found and live-validated, fail fast with a clear diagnostic instead
|
||||
* of forwarding to the known-wrong path and surfacing a raw HTML 404. The pure
|
||||
* transformation helpers (googleFlow.ts) remain fully unit-tested
|
||||
* (google-flow-video-4569.test.ts) for whenever the wire surface is fixed and this
|
||||
* handler is restored to actually submit/poll.
|
||||
*/
|
||||
|
||||
import { sanitizeErrorMessage } from "../../utils/error.ts";
|
||||
import {
|
||||
GOOGLE_FLOW_POLL_PATH,
|
||||
GOOGLE_FLOW_SUBMIT_PATH,
|
||||
buildGoogleFlowSubmitBody,
|
||||
normalizeFlowVideoParams,
|
||||
parseFlowOperationName,
|
||||
parseFlowOperationResult,
|
||||
resolveFlowAccessToken,
|
||||
resolveFlowProjectId,
|
||||
} from "./googleFlow.ts";
|
||||
import { getVideoProvider } from "../../config/videoRegistry.ts";
|
||||
|
||||
const FALLBACK_UNSUPPORTED_REASON =
|
||||
"Google Flow video generation requires a browser-session transport and is not " +
|
||||
"supported over the stored OAuth bearer.";
|
||||
|
||||
interface GoogleFlowHandlerArgs {
|
||||
model: string;
|
||||
@@ -34,112 +32,17 @@ interface GoogleFlowHandlerArgs {
|
||||
log?: { info?: (tag: string, msg: string) => void; error?: (tag: string, msg: string) => void };
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 10_000;
|
||||
const MAX_WAIT_MS = 5 * 60 * 1000;
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export async function handleGoogleFlowVideoGeneration({
|
||||
model,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}: GoogleFlowHandlerArgs) {
|
||||
const token = resolveFlowAccessToken(credentials);
|
||||
if (!token) {
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
error:
|
||||
"Missing Google OAuth token for Google Flow. Connect a Google account in Providers (the Antigravity/Cloud Code connection) first.",
|
||||
};
|
||||
}
|
||||
|
||||
const projectId = resolveFlowProjectId(credentials);
|
||||
if (!projectId) {
|
||||
return {
|
||||
success: false,
|
||||
status: 400,
|
||||
error:
|
||||
"Missing Google projectId for Google Flow. Please reconnect OAuth in Providers so OmniRoute can fetch your Cloud Code project.",
|
||||
};
|
||||
}
|
||||
|
||||
const params = normalizeFlowVideoParams(body);
|
||||
const submitBody = buildGoogleFlowSubmitBody(params);
|
||||
// PENDING LIVE VALIDATION: Cloud-Code envelope wraps the Veo body with `project`/`model`.
|
||||
const wireBody = { ...submitBody, project: projectId, model };
|
||||
|
||||
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
export async function handleGoogleFlowVideoGeneration(
|
||||
_args: GoogleFlowHandlerArgs
|
||||
): Promise<{ success: false; status: number; error: string }> {
|
||||
// #10285 — fail fast: the submit/poll wire surface is live-confirmed broken and no
|
||||
// server-side credential transport can satisfy the working endpoint (see the module
|
||||
// doc above). Do not forward to the known-wrong path / surface a raw HTML 404.
|
||||
return {
|
||||
success: false,
|
||||
status: 501,
|
||||
error: sanitizeErrorMessage(
|
||||
getVideoProvider("googleflow")?.unsupportedReason || FALLBACK_UNSUPPORTED_REASON
|
||||
),
|
||||
};
|
||||
|
||||
try {
|
||||
log?.info?.(
|
||||
"VIDEO",
|
||||
`googleflow/${model} (veo) | submitting | aspect: ${params.aspectRatio ?? "default"}`
|
||||
);
|
||||
const submitRes = await fetch(`${baseUrl}${GOOGLE_FLOW_SUBMIT_PATH}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(wireBody),
|
||||
});
|
||||
if (!submitRes.ok) {
|
||||
const errorText = await submitRes.text();
|
||||
return {
|
||||
success: false,
|
||||
status: submitRes.status,
|
||||
error: sanitizeErrorMessage(`Google Flow submit failed: ${errorText.slice(0, 300)}`),
|
||||
};
|
||||
}
|
||||
|
||||
const operationName = parseFlowOperationName(await submitRes.json());
|
||||
if (!operationName) {
|
||||
return { success: false, status: 502, error: "Google Flow did not return an operation name" };
|
||||
}
|
||||
|
||||
const deadline = Date.now() + MAX_WAIT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
const pollRes = await fetch(`${baseUrl}${GOOGLE_FLOW_POLL_PATH}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ operationName }),
|
||||
});
|
||||
if (!pollRes.ok) {
|
||||
const errorText = await pollRes.text();
|
||||
return {
|
||||
success: false,
|
||||
status: pollRes.status,
|
||||
error: sanitizeErrorMessage(`Google Flow poll failed: ${errorText.slice(0, 300)}`),
|
||||
};
|
||||
}
|
||||
|
||||
const result = parseFlowOperationResult(await pollRes.json());
|
||||
if (!result.done) continue;
|
||||
if (result.error) {
|
||||
return { success: false, status: 502, error: sanitizeErrorMessage(result.error) };
|
||||
}
|
||||
const item = result.base64
|
||||
? { b64_json: result.base64, format: result.format }
|
||||
: { url: result.url, format: result.format };
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: [item] },
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, status: 504, error: "Google Flow video generation timed out" };
|
||||
} catch (err) {
|
||||
const e = (err ?? {}) as { message?: string; status?: number };
|
||||
log?.error?.("VIDEO", `Google Flow generation failed: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
status: typeof e.status === "number" ? e.status : 502,
|
||||
error: sanitizeErrorMessage(e.message || "Google Flow generation failed"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,12 @@ import {
|
||||
resolveVideoCredentialProvider,
|
||||
} from "../../open-sse/handlers/videoGeneration/googleFlow.ts";
|
||||
|
||||
import { getVideoProvider, parseVideoModel } from "../../open-sse/config/videoRegistry.ts";
|
||||
import {
|
||||
getAllVideoModels,
|
||||
getVideoProvider,
|
||||
parseVideoModel,
|
||||
} from "../../open-sse/config/videoRegistry.ts";
|
||||
import { handleGoogleFlowVideoGeneration } from "../../open-sse/handlers/videoGeneration/googleFlowHandler.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -181,12 +186,40 @@ test("resolveFlowAccessToken: prefers accessToken, falls back to apiKey", () =>
|
||||
assert.equal(resolveFlowAccessToken({}), null);
|
||||
});
|
||||
|
||||
test("videoRegistry: googleflow provider is registered with oauth + google-flow format", () => {
|
||||
test("videoRegistry: googleflow provider is registered with oauth + google-flow format, flagged unsupported", () => {
|
||||
const provider = getVideoProvider("googleflow");
|
||||
assert.ok(provider, "googleflow provider must exist");
|
||||
assert.equal(provider.format, "google-flow");
|
||||
assert.equal(provider.authType, "oauth");
|
||||
assert.ok(provider.models.length > 0, "must expose at least one Veo model");
|
||||
// #10285 — the submit/poll endpoints are live-confirmed wrong and no server-side
|
||||
// OAuth bearer can satisfy the working endpoint (aisandbox-pa rejects it). Until a
|
||||
// viable transport exists the provider must not be presented as functional.
|
||||
assert.equal(provider.unsupported, true, "googleflow must be flagged unsupported (#10285)");
|
||||
assert.match(provider.unsupportedReason ?? "", /browser-session|not supported/i);
|
||||
});
|
||||
|
||||
test("videoRegistry: getAllVideoModels excludes the unsupported googleflow provider (#10285)", () => {
|
||||
const models = getAllVideoModels();
|
||||
const flowModels = models.filter(
|
||||
(m) => m.provider === "googleflow" || m.id.startsWith("googleflow/") || m.id.startsWith("flow/")
|
||||
);
|
||||
assert.deepEqual(flowModels, [], "googleflow must not be advertised in /v1/models until fixed");
|
||||
// Sanity: other providers are still listed, so exclusion is targeted, not global.
|
||||
assert.ok(models.some((m) => m.provider === "vertex"), "unrelated providers must stay listed");
|
||||
});
|
||||
|
||||
test("handleGoogleFlowVideoGeneration: fails fast with a clear diagnostic instead of the wrong path (#10285)", async () => {
|
||||
const result = await handleGoogleFlowVideoGeneration({
|
||||
model: "veo-3.1-generate",
|
||||
providerConfig: { baseUrl: "https://aisandbox-pa.googleapis.com" },
|
||||
body: { prompt: "a cat surfing" },
|
||||
credentials: { accessToken: "tok", projectId: "proj-1" },
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 501);
|
||||
assert.match(result.error ?? "", /browser-session|not supported/i);
|
||||
assert.doesNotMatch(result.error ?? "", /<!DOCTYPE/i, "must not surface a raw HTML 404 body");
|
||||
});
|
||||
|
||||
test("videoRegistry: parseVideoModel resolves googleflow/<model> and its alias", () => {
|
||||
|
||||
Reference in New Issue
Block a user