Skip to content
Closed
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --varia
| `antigravity-gemini-3-flash` | minimal, low, medium, high | Gemini 3 Flash with thinking |
| `antigravity-claude-sonnet-4-6` | — | Claude Sonnet 4.6 |
| `antigravity-claude-opus-4-6-thinking` | low, max | Claude Opus 4.6 with extended thinking |
| `antigravity-gemini-3.1-flash-image` | minimal, high | Gemini 3.1 Flash image generation |

**Gemini CLI quota** (separate from Antigravity; used when `cli_first` is true or as fallback):

Expand All @@ -148,6 +149,30 @@ opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --varia

For details on variant configuration and thinking levels, see [docs/MODEL-VARIANTS.md](docs/MODEL-VARIANTS.md).

**Image generation:**

Select an image model and include your prompt. Images are saved to `./nanobanana/` in your project directory.

```bash
opencode run "a realistic animal sitting by a window, soft streetlight, bokeh" --model=google/antigravity-gemini-3.1-flash-image
```

Use `--resolution` and `--aspect-ratio` flags inline to override defaults:

```bash
# 4K resolution with 16:9 aspect ratio
opencode run "mountain landscape --resolution=4K --aspect-ratio=16:9" --model=google/antigravity-gemini-3.1-flash-image
```

Flags are stripped from the prompt before sending to Gemini. Defaults can also be set via environment variables:

| Flag / Env Var | Values | Default |
|----------------|--------|---------|
| `--resolution` / `OPENCODE_IMAGE_SIZE` | `0.5K`\*, `1K`, `2K`, `4K` | `1K` |
| `--aspect-ratio` / `OPENCODE_IMAGE_ASPECT_RATIO` | `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`, `4:1`\*, `8:1`\* | `1:1` |

\* `0.5K` and extended aspect ratios (`4:1`, `8:1`) are only supported by `gemini-3.1-flash-image`.

<details>
<summary><b>Full models configuration (copy-paste ready)</b></summary>

Expand Down Expand Up @@ -203,6 +228,15 @@ Add this to your `~/.config/opencode/opencode.json`:
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
},
"antigravity-gemini-3.1-flash-image": {
"name": "Gemini 3.1 Flash Image (Antigravity)",
"limit": { "context": 131072, "output": 32768 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text", "image"] },
"variants": {
"minimal": { "thinkingLevel": "minimal" },
"high": { "thinkingLevel": "high" }
}
},
"gemini-2.5-flash": {
"name": "Gemini 2.5 Flash (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/plugin/accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,18 +1163,18 @@ describe("AccountManager", () => {
const manager = new AccountManager(undefined, stored);
const account = manager.getCurrentOrNextForFamily("gemini");

manager.markRateLimited(account!, 30000, "gemini", "antigravity", "gemini-3-pro-image");
manager.markRateLimited(account!, 30000, "gemini", "antigravity", "gemini-3.1-flash-image");

expect(
manager.getMinWaitTimeForFamily(
"gemini",
"gemini-3-pro-image",
"gemini-3.1-flash-image",
"antigravity",
true,
),
).toBe(30000);

expect(manager.getMinWaitTimeForFamily("gemini", "gemini-3-pro-image")).toBe(0);
expect(manager.getMinWaitTimeForFamily("gemini", "gemini-3.1-flash-image")).toBe(0);
});

describe("parseRateLimitReason", () => {
Expand Down
1 change: 1 addition & 0 deletions src/plugin/config/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("OPENCODE_MODEL_DEFINITIONS", () => {
"antigravity-claude-sonnet-4-6",
"antigravity-gemini-3-flash",
"antigravity-gemini-3-pro",
"antigravity-gemini-3.1-flash-image",
"antigravity-gemini-3.1-pro",
"gemini-2.5-flash",
"gemini-2.5-pro",
Expand Down
12 changes: 12 additions & 0 deletions src/plugin/config/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ export const OPENCODE_MODEL_DEFINITIONS: OpencodeModelDefinitions = {
high: { thinkingLevel: "high" },
},
},
"antigravity-gemini-3.1-flash-image": {
name: "Gemini 3.1 Flash Image (Antigravity)",
limit: { context: 131072, output: 32768 },
modalities: {
input: ["text", "image", "pdf"],
output: ["text", "image"],
},
variants: {
minimal: { thinkingLevel: "minimal" },
high: { thinkingLevel: "high" },
},
},
"antigravity-claude-sonnet-4-6": {
name: "Claude Sonnet 4.6 (Antigravity)",
limit: { context: 200000, output: 64000 },
Expand Down
7 changes: 3 additions & 4 deletions src/plugin/image-saver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
* Image Saving Utility
*
* Handles saving generated images to disk and returning file paths.
* Images are saved to ./nanobanana/ relative to the current working directory.
*/

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

/**
* Default directory for saving generated images.
* Uses ~/.opencode/generated-images/
* Uses ./nanobanana/ relative to the current working directory (process.cwd()).
*/
function getImageOutputDir(): string {
const homeDir = os.homedir();
const outputDir = path.join(homeDir, '.opencode', 'generated-images');
const outputDir = path.join(process.cwd(), 'nanobanana');

// Create directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
Expand Down
66 changes: 58 additions & 8 deletions src/plugin/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ import {
needsThinkingRecovery,
} from "./thinking-recovery";
import { sanitizeCrossModelPayloadInPlace } from "./transform/cross-model-sanitizer";
import { isGemini3Model, isImageGenerationModel, buildImageGenerationConfig, applyGeminiTransforms } from "./transform";
import { isGemini3Model, isImageGenerationModel, isFlashImageModel, buildImageGenerationConfig, applyGeminiTransforms } from "./transform";
import { parsePromptFlags, extractLastUserPrompt } from "./transform/prompt-flags";
import {
resolveModelWithTier,
resolveModelWithVariant,
Expand Down Expand Up @@ -956,25 +957,58 @@ export function prepareAntigravityRequest(
}

// Resolve thinking configuration based on user settings and model capabilities
// Image generation models don't support thinking - skip thinking config entirely
// Pro image models don't support thinking. Flash image models support minimal/high.
const isImageModel = isImageGenerationModel(effectiveModel);
const userThinkingConfig = isImageModel ? undefined : extractThinkingConfig(requestPayload, rawGenerationConfig, extraBody);
const isFlashImage = isImageModel && isFlashImageModel(effectiveModel);
const skipThinkingForImage = isImageModel && !isFlashImage;
const userThinkingConfig = skipThinkingForImage ? undefined : extractThinkingConfig(requestPayload, rawGenerationConfig, extraBody);
const hasAssistantHistory = Array.isArray(requestPayload.contents) &&
requestPayload.contents.some((c: any) => c?.role === "model" || c?.role === "assistant");

// Claude Sonnet 4.6 is non-thinking only.
// Ignore any client-provided thinkingConfig for this model.
const lowerEffective = effectiveModel.toLowerCase();
const isClaudeSonnetNonThinking = lowerEffective === "claude-sonnet-4-6";
const effectiveUserThinkingConfig = (isClaudeSonnetNonThinking || isImageModel) ? undefined : userThinkingConfig;
const effectiveUserThinkingConfig = (isClaudeSonnetNonThinking || skipThinkingForImage) ? undefined : userThinkingConfig;

// For image models, add imageConfig instead of thinkingConfig
// For image models, add imageConfig (and optionally thinkingConfig for flash-image)
if (isImageModel) {
const imageConfig = buildImageGenerationConfig();
// Parse --resolution and --aspect-ratio flags from the last user prompt
let imageConfigOverrides: { aspectRatio?: string; imageSize?: string } | undefined;
if (Array.isArray(requestPayload.contents)) {
const lastPrompt = extractLastUserPrompt(requestPayload.contents as unknown[]);
if (lastPrompt) {
const parsed = parsePromptFlags(lastPrompt.text);
if (parsed.resolution || parsed.aspectRatio) {
imageConfigOverrides = {
imageSize: parsed.resolution,
aspectRatio: parsed.aspectRatio,
};
// Replace prompt text with flags stripped out
const content = (requestPayload.contents as any[])[lastPrompt.contentIndex];
if (content?.parts?.[lastPrompt.partIndex]) {
content.parts[lastPrompt.partIndex].text = parsed.cleanedPrompt;
}
log.debug(`[image] Parsed prompt flags: resolution=${parsed.resolution ?? "default"}, aspectRatio=${parsed.aspectRatio ?? "default"}`);
}
}
}

const imageConfig = buildImageGenerationConfig(effectiveModel, imageConfigOverrides);
const generationConfig = (rawGenerationConfig ?? {}) as Record<string, unknown>;
generationConfig.imageConfig = imageConfig;
// Remove any thinkingConfig that might have been set
delete generationConfig.thinkingConfig;

// Flash image models support thinking (minimal/high)
if (isFlashImage && resolved.isThinkingModel && resolved.thinkingLevel) {
generationConfig.thinkingConfig = {
includeThoughts: true,
thinkingLevel: resolved.thinkingLevel,
};
} else {
// Remove any thinkingConfig for non-thinking image models
delete generationConfig.thinkingConfig;
}

// Set reasonable defaults for image generation
if (!generationConfig.candidateCount) {
generationConfig.candidateCount = 1;
Expand Down Expand Up @@ -1750,6 +1784,22 @@ export async function transformAntigravityResponse(
headers.set("x-antigravity-context-error", "tool_pairing");
}

// Detect imageSize / imageConfig errors from Antigravity API
// If the endpoint doesn't support imageSize, strip it and let the user know
if (
response.status === 400 &&
(errorMessage.includes("imagesize") ||
errorMessage.includes("image_size") ||
(errorMessage.includes("imageconfig") && errorMessage.includes("invalid")))
) {
headers.set("x-antigravity-image-error", "imagesize_unsupported");
console.warn(
`[image] imageSize rejected by API (model: ${effectiveModel || "unknown"}). ` +
`The Antigravity endpoint may not support imageSize for this model. ` +
`Unset OPENCODE_IMAGE_SIZE to use the default 1K resolution.`
);
}

return new Response(JSON.stringify(errorBody), {
status: response.status,
statusText: response.statusText,
Expand Down
Loading