Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"_rebaseline_2026_06_15_3877_byteplus": "Issue #3877 own growth: providerRegistry.ts 4708->4730 (+22 = a byteplus (BytePlus ModelArk) registry entry — OpenAI-compatible, Ark base ap-southeast-1, Bearer, 4 seed models — modeled on the volcengine entry; byteplus was in APIKEY_PROVIDERS but unregistered here so validation returned {unsupported:true}). Cohesive provider registration; not extractable.",
"_rebaseline_2026_06_15_3910_nested_combo_ctx": "PR #3910 net +1: providerRegistry.ts 4730->4731 (test-registry contextLength metadata for the nested combo-ref LCD regression test). opencode-plugin-only behavioral change; no core routing/virtualFactory touched.",
"_rebaseline_2026_06_15_3929_vertex_media": "PR #3929 own growth: audioSpeech.ts 952->965 (+13) and videoGeneration.ts 1026->1078 (+52) = vertex/* media branches (Gemini TTS, Veo predictLongRunning poll) wired into the speech/video handlers; new logic lives in open-sse/executors/vertexMedia.ts (341, under cap). Cohesive media-provider feature.",
"_rebaseline_2026_06_23_4569_googleflow_video": "Issue #4569 net +5: videoGeneration.ts 1077->1083 = the google-flow dispatch branch + a single handler import. The whole Google Flow handler (145 lines) lives in the new co-located open-sse/handlers/videoGeneration/googleFlowHandler.ts (under cap) and the pure transforms in videoGeneration/googleFlow.ts; only the wiring remains in the god-file. Extracted-not-inlined; not further shrinkable.",
"_rebaseline_2026_06_15_3879_redact_thinking": "PR #3879 + #3921 reconcile: AddApiKeyModal.tsx 843->845 (+2 = merging #3879's CcCompatibleRequestDefaultsFields (context1m + opt-in redact-thinking toggle) into #3921's preset-input block in the cc-compatible settings group). Cohesive UI; not extractable.",
"_rebaseline_2026_06_15_3890_cache_preserve": "Issue #3890 own growth: chatCore.ts 5815->5823 (+8 = wire resolveCacheAwareConfig() into the compression apply step so the system prompt is never compressed in a caching context — honors the cache-aware skipSystemPrompt flag that selectCompressionStrategy could not carry). Cohesive cache-preservation guard at the existing compression chokepoint; not extractable.",
"_rebaseline_2026_06_16_3974_toolsearch_beta": "Issue #3974 own growth: base.ts 1218->1222 (+4 = wrap selectBetaFlags() with mergeClientAnthropicBeta() at the ccHeaders anthropic-beta callsite, plus a 3-line comment, so the client's allowlisted tool-search beta survives). The shared helper + allowlist live in anthropicHeaders.ts (small file, well under cap); default.ts also gains the merge. Cohesive one-callsite fix; not extractable.",
Expand Down Expand Up @@ -128,7 +129,7 @@
"open-sse/handlers/responseSanitizer.ts": 1103,
"open-sse/handlers/search.ts": 1546,
"open-sse/handlers/sseParser.ts": 830,
"open-sse/handlers/videoGeneration.ts": 1078,
"open-sse/handlers/videoGeneration.ts": 1083,
"open-sse/mcp-server/schemas/tools.ts": 1497,
"open-sse/mcp-server/server.ts": 1555,
"open-sse/mcp-server/tools/advancedTools.ts": 1118,
Expand Down
17 changes: 17 additions & 0 deletions open-sse/config/videoRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
],
},

googleflow: {
id: "googleflow",
alias: "flow",
// ⚠️ Wire host isolated for live HAR validation (Rule #18). The handler reuses
// the Google account OAuth credential (accessToken + Cloud Code projectId) that
// the Antigravity provider already establishes; no separate OAuth flow is added.
baseUrl: "https://aisandbox-pa.googleapis.com",
authType: "oauth",
authHeader: "bearer",
format: "google-flow",
models: [
{ id: "veo-3.1-generate", name: "Veo 3.1 (Google Flow)" },
{ id: "veo-3.1-fast-generate", name: "Veo 3.1 Fast (Google Flow)" },
{ id: "veo-3.0-generate", name: "Veo 3.0 (Google Flow)" },
],
},

kie: {
id: "kie",
baseUrl: "https://api.kie.ai",
Expand Down
5 changes: 5 additions & 0 deletions open-sse/handlers/videoGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts";
import { kieExecutor } from "../executors/kie.ts";
import { vertexGenerateVideo } from "../executors/vertexMedia.ts";
import { handleGoogleFlowVideoGeneration } from "./videoGeneration/googleFlowHandler.ts";
import { getExecutor } from "../executors/index.ts";
import { isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
import {
Expand Down Expand Up @@ -61,6 +62,10 @@ export async function handleVideoGeneration({ body, credentials, log }) {
return handleVertexVeoGeneration({ model, body, credentials, log });
}

if (providerConfig.format === "google-flow") {
return handleGoogleFlowVideoGeneration({ model, providerConfig, body, credentials, log });
}

if (providerConfig.format === "comfyui") {
return handleComfyUIVideoGeneration({ model, provider, providerConfig, body, log });
}
Expand Down
219 changes: 219 additions & 0 deletions open-sse/handlers/videoGeneration/googleFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* Google Flow (labs.google/flow) video generation — pure helpers.
*
* Google Flow drives Veo video generation through Google's internal AI Sandbox
* endpoint (`aisandbox-pa.googleapis.com`) using a Google account OAuth bearer
* token — the same Google OAuth credential family that the Antigravity provider
* already uses (accessToken + Cloud Code projectId). The request/response shape
* mirrors the *documented* Veo `predictLongRunning` long-running-operation API
* (see `open-sse/executors/vertexMedia.ts::vertexGenerateVideo`):
*
* submit → { instances: [{ prompt, image? }], parameters: { sampleCount, ... } } → { name: <operation> }
* poll → { operationName } → { done, error?, response: { videos: [{ bytesBase64Encoded | gcsUri }] } }
*
* ⚠️ PENDING LIVE VALIDATION (Hard Rule #18): the exact AI-Sandbox URL path and
* whether Flow wraps the Veo body in the Cloud-Code `{ project, request }` envelope
* cannot be unit-tested — they require a real Google Flow account + a captured HAR.
* Everything that depends on the wire host/path is isolated in the two constants
* below and in the handler, so confirming a captured HAR is a one-line change.
* The transformation logic in this file is grounded in the documented Veo shape
* and is fully unit-tested.
*/

/**
* Google Flow has no standalone connection — it reuses the Antigravity Google
* OAuth credential (accessToken + Cloud Code projectId). Credential lookups for
* the `googleflow` provider resolve against this provider id.
*/
export const GOOGLE_FLOW_CREDENTIAL_PROVIDER = "antigravity";

/** Map a video provider id to the provider id whose stored credentials it uses. */
export function resolveVideoCredentialProvider(provider: string): string {
return provider === "googleflow" ? GOOGLE_FLOW_CREDENTIAL_PROVIDER : provider;
}

// --- Wire endpoint (isolated; confirm against a real Flow HAR — Rule #18) ---
export const GOOGLE_FLOW_HOST = "https://aisandbox-pa.googleapis.com";
export const GOOGLE_FLOW_SUBMIT_PATH = "/v1:generateVideo";
export const GOOGLE_FLOW_POLL_PATH = "/v1:fetchOperation";

export interface FlowVideoParams {
prompt: string;
aspectRatio?: string;
durationSeconds?: number;
sampleCount: number;
negativePrompt?: string;
resolution?: string;
}

export interface FlowOperationResult {
done: boolean;
error?: string;
base64?: string;
url?: string;
format?: string;
}

function asTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}

const ASPECT_RATIO_RE = /^\d{1,2}:\d{1,2}$/;

/**
* Normalize an OpenAI-style /v1/videos/generations body into Veo parameters.
* Accepts both snake_case (OpenAI) and camelCase (native) field names, and
* treats a `size` that looks like a ratio (e.g. "16:9") as the aspect ratio.
*/
export function normalizeFlowVideoParams(body: Record<string, unknown> | null | undefined): FlowVideoParams {
const b = body ?? {};
const prompt = typeof b.prompt === "string" ? b.prompt : String(b.prompt ?? "");

const sizeMaybeRatio = asTrimmedString(b.size);
const aspectRatio =
asTrimmedString(b.aspect_ratio) ??
asTrimmedString(b.aspectRatio) ??
(sizeMaybeRatio && ASPECT_RATIO_RE.test(sizeMaybeRatio) ? sizeMaybeRatio : undefined);

const durationRaw =
typeof b.duration === "number"
? b.duration
: typeof b.durationSeconds === "number"
? b.durationSeconds
: undefined;
const durationSeconds =
typeof durationRaw === "number" && Number.isFinite(durationRaw) && durationRaw > 0
? Math.floor(durationRaw)
: undefined;

const nRaw = typeof b.n === "number" ? b.n : undefined;
const sampleCount =
typeof nRaw === "number" && Number.isFinite(nRaw) && nRaw > 0 ? Math.floor(nRaw) : 1;

return {
prompt,
aspectRatio,
durationSeconds,
sampleCount,
negativePrompt: asTrimmedString(b.negative_prompt) ?? asTrimmedString(b.negativePrompt),
resolution: asTrimmedString(b.resolution),
};
}

/**
* Build the documented Veo `predictLongRunning` request body for the submit call.
* Only includes optional parameters that were actually provided.
*/
export function buildGoogleFlowSubmitBody(params: FlowVideoParams): {
instances: Array<Record<string, unknown>>;
parameters: Record<string, unknown>;
} {
const parameters: Record<string, unknown> = { sampleCount: params.sampleCount };
if (params.aspectRatio) parameters.aspectRatio = params.aspectRatio;
if (typeof params.durationSeconds === "number") parameters.durationSeconds = params.durationSeconds;
if (params.negativePrompt) parameters.negativePrompt = params.negativePrompt;
if (params.resolution) parameters.resolution = params.resolution;

return {
instances: [{ prompt: params.prompt }],
parameters,
};
}

/**
* Extract the long-running-operation name from a submit response.
* Tolerates both `{ name }` and `{ operation: { name } }` envelopes.
*/
export function parseFlowOperationName(json: unknown): string | null {
if (!json || typeof json !== "object") return null;
const direct = (json as { name?: unknown }).name;
if (typeof direct === "string" && direct.length > 0) return direct;
const op = (json as { operation?: { name?: unknown } }).operation;
if (op && typeof op === "object" && typeof op.name === "string" && op.name.length > 0) {
return op.name;
}
return null;
}

function extractVideoFromResponse(response: unknown): { base64?: string; url?: string } | null {
if (!response || typeof response !== "object") return null;

// Documented Veo LRO shape: response.videos[].bytesBase64Encoded | gcsUri
const videos = (response as { videos?: unknown }).videos;
if (Array.isArray(videos) && videos.length > 0) {
const v = videos[0];
if (v && typeof v === "object") {
const rec = v as Record<string, unknown>;
if (typeof rec.bytesBase64Encoded === "string") return { base64: rec.bytesBase64Encoded };
if (typeof rec.gcsUri === "string") return { url: rec.gcsUri };
if (typeof rec.uri === "string") return { url: rec.uri };
}
}

// Alternate Veo shape: response.generateVideoResponse.generatedSamples[].video.uri
const gen = (response as { generateVideoResponse?: { generatedSamples?: unknown } })
.generateVideoResponse;
const samples = gen && typeof gen === "object" ? gen.generatedSamples : undefined;
if (Array.isArray(samples) && samples.length > 0) {
const sample = samples[0];
const video =
sample && typeof sample === "object"
? (sample as { video?: unknown }).video
: undefined;
if (video && typeof video === "object") {
const rec = video as Record<string, unknown>;
if (typeof rec.uri === "string") return { url: rec.uri };
if (typeof rec.bytesBase64Encoded === "string") return { base64: rec.bytesBase64Encoded };
}
}

return null;
}
Comment thread
diegosouzapw marked this conversation as resolved.

/**
* Interpret a poll/fetch-operation response into a normalized result.
* `done: false` means still running; callers should keep polling.
*/
export function parseFlowOperationResult(json: unknown): FlowOperationResult {
if (!json || typeof json !== "object") return { done: false };

const done = Boolean((json as { done?: unknown }).done);
if (!done) return { done: false };

const opError = (json as { error?: { message?: unknown } }).error;
if (opError && typeof opError === "object") {
return { done: true, error: String(opError.message || "Google Flow video operation failed") };
}

const response = (json as { response?: unknown }).response;
const video = extractVideoFromResponse(response);
if (video?.base64) return { done: true, base64: video.base64, format: "mp4" };
if (video?.url) return { done: true, url: video.url, format: "mp4" };

return { done: true, error: "Google Flow operation completed but returned no video" };
}

/** Resolve the Cloud Code projectId from the OAuth credential record (mirrors Antigravity). */
export function resolveFlowProjectId(
credentials: Record<string, unknown> | null | undefined
): string | null {
const cred = credentials ?? {};
const direct = asTrimmedString(cred.projectId);
if (direct) return direct;
const psd = cred.providerSpecificData;
if (psd && typeof psd === "object") {
const fromPsd = asTrimmedString((psd as Record<string, unknown>).projectId);
if (fromPsd) return fromPsd;
}
return null;
}

/** Resolve the OAuth bearer token from the credential record. */
export function resolveFlowAccessToken(
credentials: Record<string, unknown> | null | undefined
): string | null {
const cred = credentials ?? {};
return asTrimmedString(cred.accessToken) ?? asTrimmedString(cred.apiKey) ?? null;
}
Loading
Loading