diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..847aeb6 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,42 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + cache: true + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: release-artifacts + path: | + dist/ + !dist/*.txt + retention-days: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 8a82972..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Release Binaries - -on: - release: - types: [created] - workflow_dispatch: - -permissions: - contents: write - -jobs: - build-and-release: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Build binaries - run: bash scripts/compile-all.sh - - - name: Upload binaries to release - run: | - # If triggered by release, upload to that release. - # If triggered by workflow_dispatch, we can try to upload to the "latest" release if it exists, - # or just skip upload if we don't have a tag. - if [ "${{ github.event_name }}" = "release" ]; then - gh release upload ${{ github.event.release.tag_name }} dist/gemini-api-* --clobber - else - echo "Workflow dispatched manually. Skipping release upload as there is no active release event." - echo "Built binaries:" - ls -lh dist/ - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e70039e..0000000 --- a/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -node_modules/ -dist/ -.gemini/ -*.tgz -.DS_Store - -tasks/ -CLI-skill.md -SPEC.md -PROGRESS.md -PROMPT.md -old-cli -REVIEW.md -tmp/ -templates/ \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..0d61930 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,52 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - id: gemini-api + main: ./cmd/gemini-api + binary: gemini-api + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X main.buildTime={{.Date}} + +archives: + - id: gemini-api + formats: [tar.gz] + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + formats: [zip] + files: + - README.md + - LICENSE* + +release: + github: + owner: google-gemini + name: gemini-api-cli + draft: false + prerelease: auto + mode: append + +checksum: + name_template: "checksums.txt" diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 808c84b..0000000 --- a/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -dist/gemini-api -dist/gemini-api-darwin-arm64 -dist/gemini-api-darwin-x64 -dist/gemini-api-linux-arm64 -dist/gemini-api-linux-x64 -dist/gemini-api-win-x64.exe -dist/gemini-api-min diff --git a/DOCS.md b/DOCS.md deleted file mode 100644 index 9191b9a..0000000 --- a/DOCS.md +++ /dev/null @@ -1,582 +0,0 @@ -# Experimental Gemini API CLI — Documentation - -> [!CAUTION] -> **Disclaimer**: This is not a supported Google product. - -> Develop, test, and deploy Gemini Agents. Run interactions across every model and modality. - ---- - -## Installation - -### Via Install Script (Recommended) - -You can install the pre-compiled binary directly (no cloning required) using this single-line command: - -```bash -curl -fsSL https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.sh | bash -``` - -### From Source (via npm) - -```bash -git clone https://github.com/google-gemini/gemini-api-cli.git -cd gemini-api-cli -npm install -g . -``` - -**Requirements:** Bun ≥ 1.1 or Node.js ≥ 22 - ---- - -## Authentication - -```bash -# Option 1: Environment variable (recommended) -export GEMINI_API_KEY="your-api-key" - -# Option 2: Flag (works with any command) -gemini-api run "Hello" --api-key "your-api-key" -``` - -Get your API key at [aistudio.google.com](https://aistudio.google.com/). - ---- - -## Quick Start - -```bash -# Run a prompt against a model -gemini-api run "What is the capital of France?" - -# Use a specific model -gemini-api run "Explain quantum computing" --model gemini-3.1-pro-preview - -# Scaffold an agent -gemini-api agents init my-agent -cd my-agent - -# Edit agent.yaml and AGENTS.md, then test locally -gemini-api agents test --prompt "Hello, what can you do?" - -# Deploy to the platform -gemini-api agents create - -# Test the deployed agent -gemini-api run "Hello" --agent my-agent - -# List and manage agents -gemini-api agents list -gemini-api agents delete my-agent -``` - ---- - -## Commands - -### `gemini-api run ` - -Create an interaction against a model or agent. - -```bash -gemini-api run "What is the capital of France?" -gemini-api run "Explain this code" --model gemini-3.1-pro-preview -gemini-api run "Analyze my data" --agent my-data-analyst -``` - -| Flag | Short | Type | Default | Description | -|---|---|---|---|---| -| `` | | positional | — | Input prompt. | -| `--model` | `-m` | string | `gemini-3.5-flash` | Model to use | -| `--agent` | `-a` | string | — | Agent to use (overrides `--model`) | -| `--input` | `-i` | string[] | — | Multimodal input: `image:path`, `audio:path`, `video:path`, `document:path` | -| `--output` | `-o` | string | — | Save generated media to file | - -| `--previous-interaction-id` | `-p` | string | — | Continue from previous interaction | -| `--system-instruction` | `-s` | string | — | System instruction | -| `--response-modality` | | enum[] | — | `text`, `image`, `audio`, `video`, `document` | -| `--response-mime-type` | | string | — | MIME type for response | -| `--tool` | | string[] | — | Tool declaration (can be repeated): `code_execution`, `google_search`, `mcp_server:name:url` | -| `--source` | | string[] | — | Environment source (can be repeated): `inline:target:content`, `github:url:target`, `gcs:source:target` | - -| `--voice` | | string | — | TTS voice name | -| `--language` | | string | — | TTS language code | -| `--aspect-ratio` | | enum | — | Image aspect ratio (e.g., `16:9`) | -| `--image-size` | | enum | — | `512`, `1K`, `2K`, `4K` | -| `--edit-strength` | | float | — | How much to change the original image (0.0 to 1.0) | -| `--mask` | | string | — | Path to a mask image for localized editing | -| `--service-tier` | | enum | — | `flex`, `standard`, `priority` | -| `--json` | `-j` | boolean | `false` | Output raw SSE events as JSONL | -| `--dry-run` | | boolean | `false` | Print curl command and exit | - -| `--api-key` | | string | `$GEMINI_API_KEY` | API key | -| `--base-url` | | string | `$GEMINI_API_BASE_URL` | Override API base URL | - - -**Examples:** - -```bash -# Model interaction -gemini-api run "Write a haiku about code" - -# Image understanding -gemini-api run "What's in this image?" --input image:photo.jpg - -# Image editing -gemini-api run "Add a red hat" --input image:person.jpg --response-modality image --output with_hat.jpg - -# Image generation -gemini-api run "A cat in space" --model gemini-3.1-flash-image-preview --output cat.png - -# Text-to-speech -gemini-api run "Hello my name is gemini, i am a large language model from google. I can help you with a wide range of tasks." --model gemini-3.1-flash-tts-preview --voice Kore --output hello.wav - -# With tools -gemini-api run "What is the weather?" --tool google_search --tool code_execution - -# With sources (e.g. antigravity-preview-05-2026) -gemini-api run "Generate test" --agent antigravity-preview-05-2026 --source "inline:/.agents/README.md:# Instructions" --source "github:https://github.com/user/repo:/.agents" - -# Multi-turn -gemini-api run "Remember the word: banana" -# → interaction_id: int_abc123 -gemini-api run "What word?" --previous-interaction-id int_abc123 - -# Dry run -gemini-api run "Hello" --dry-run -``` - ---- - -### `gemini-api agents` - -Manage the full agent lifecycle. - -#### `gemini-api agents init ` - -Scaffold a new agent project. - -```bash -gemini-api agents init my-agent -gemini-api agents init my-agent --base-agent antigravity-preview-05-2026 -gemini-api agents init my-agent --from-template https://github.com/google-gemini/Gemini-API-Agent-Templates/tree/main/customer-data-analysis-agent -``` - -| Flag | Type | Default | Description | -|---|---|---|---| -| `` | positional | — | Agent directory name | -| `--base-agent` | string | `antigravity-preview-05-2026` | Base model (only 'antigravity-preview-05-2026' is supported) | -| `--from-template` | string | — | Git or GCS URL to scaffold from | - -#### `gemini-api agents create` - -Deploy agent from current directory. - -```bash -gemini-api agents create -gemini-api agents create --path ./my-agent -gemini-api agents create --dry-run -``` - -| Flag | Type | Default | Description | -|---|---|---|---| -| `--path` | string | `.` | Agent directory | -| `--base-env` | string | — | Override base environment | -| `--dry-run` | boolean | `false` | Print curl | -| `--json` | boolean | `false` | JSON output | - -#### `gemini-api agents list` - -```bash -gemini-api agents list -gemini-api agents list --json -gemini-api agents list --dry-run -``` - -#### `gemini-api agents get ` - -```bash -gemini-api agents get my-agent -gemini-api agents get my-agent --json -gemini-api agents get my-agent --dry-run -``` - -#### `gemini-api agents delete ` - -```bash -gemini-api agents delete my-agent -gemini-api agents delete my-agent --force -gemini-api agents delete my-agent --dry-run -``` - -| Flag | Type | Default | Description | -|---|---|---|---| -| `--force` | boolean | `false` | Skip confirmation | - -#### `gemini-api agents test` - -Run an interaction using local agent config. - -```bash -gemini-api agents test --prompt "Hello" -gemini-api agents test --prompt "Hello" --path ./my-agent -gemini-api agents test --prompt "Continue" --previous-interaction-id int_abc --environment env_xyz -gemini-api agents test --prompt "Hello" --dry-run -``` - -| Flag | Type | Default | Description | -|---|---|---|---| -| `--prompt` | string | — | Input prompt (required) | -| `--path` | string | `.` | Agent directory | - -| `--previous-interaction-id` | string | — | Multi-turn | -| `--environment` | string | — | Use existing environment | -| `--json` | boolean | `false` | JSON output | -| `--dry-run` | boolean | `false` | Print curl | - - ---- - -### `gemini-api files` - -Manage environment files. - -#### `gemini-api files download ` - -Download all files from the environment as a snapshot and extract them into a folder named `snapshot_` in the output directory. - -```bash -gemini-api files download env_xyz789 -gemini-api files download env_xyz789 --output ./results -gemini-api files download env_xyz789 --dry-run -``` - -| Flag | Type | Default | Description | -|---|---|---|---| -| `--output` | string | `./` | Output directory | - ---- - -## Agent Configuration - -### Directory Structure - -``` -my-agent/ -├── agent.yaml # Configuration (not inlined) -├── AGENTS.md # System instructions (inlined to /.agents/AGENTS.md) -├── skills/ # Custom skills (all files inlined recursively) -└── workspace/ # Files seeded into remote environment (all files inlined recursively) -``` - -### `agent.yaml` - -```yaml -# Required -id: my-agent -base_agent: antigravity-preview-05-2026 - -# Optional -description: "A data analyst agent" -instructions: "You are a helpful assistant." - -# Tools -tools: - - type: code_execution - - type: google_search - -# Environment -environment: remote - -# OR derive from existing environment -# base_environment: env_abc123 -``` - - - -### `AGENTS.md` - -Agent instructions in markdown. Uploaded to the remote environment and loaded before running. Use for long instructions — easier to read, diff, and version than `instructions` in `agent.yaml`. - -### `workspace/` - -Files seeded into the remote environment at `/.agents/workspace/`. All files in this directory are inlined into the API request when running `agents test` or `agents create`. - -**File handling:** - -| File type | How it's sent | Example extensions | -|---|---|---| -| Text files | Inlined as UTF-8 strings | `.md`, `.py`, `.csv`, `.json`, `.yaml` | -| Binary files | Base64-encoded with `"encoding": "base64"` | `.pdf`, `.png`, `.jpg`, `.mp3`, `.wav`, `.zip` | -| Files > 1 MB | Skipped | — | - -> **Note:** Only `AGENTS.md`, `workspace/`, and `skills/` are inlined from the agent directory. All other root-level files and directories are ignored. - -Binary files are automatically detected by extension. The following are treated as binary: -- Images: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.bmp`, `.tiff`, `.heic`, `.heif` -- Audio: `.wav`, `.mp3`, `.aac`, `.ogg`, `.flac`, `.opus`, `.m4a` -- Video: `.mp4`, `.mov`, `.avi`, `.webm`, `.wmv` -- Documents: `.pdf` -- Archives: `.zip`, `.tar`, `.gz`, `.bz2`, `.xz`, `.7z` - -### `environment` (in `agent.yaml`) - -Controls the sandbox environment for the agent: - -```yaml -# Enable a managed sandbox environment -environment: remote - -# OR reuse an existing environment by ID -# base_environment: env_abc123 -``` - -When `environment` is `"remote"`, the API provisions a sandbox with code execution capabilities. Workspace files and skills are seeded into it before the agent runs. - -You can also specify a structured config object to configure GCS/GitHub `sources`, establish `network` allowlists, and inject secret credentials securely via header `transform` rules: - -```yaml -environment: - type: "remote" - # Sources to copy or clone into the environment on startup - sources: - - type: "gcs" - source: "gs://my-bucket-name/folder/" - target: ".agents/workspace" - - type: "github" - source: "https://github.com/my-username/my-repo" - target: ".agents/workspace/repo" - - # Outbound network security policies and headers injection (secrets) - network: - allowlist: - - domain: "api.github.com" - transform: - # Injects Authorization header dynamically at egress proxy level - Authorization: "Bearer your-github-token" - - domain: "storage.googleapis.com" - transform: - Authorization: "Bearer your-gcloud-oauth-token" - - domain: "*.wikipedia.org" - # Catch-all rule (optional) to allow other traffic without header injection - - domain: "*" -``` - ---- - -## Tools - - - -### `agent.yaml` Tools - -```yaml -tools: - - type: code_execution - - type: google_search - search_types: [web_search, image_search] - - type: url_context -``` - ---- - -## Output Modes - -### Normal (Default) - -Optimized for clean, readable output and valid Markdown parsing. Thoughts are concise, tool calls are consolidated into single lines, and the final response text is printed without leading indentation: - -``` -[thought] -[tool] write_file(path="hello.py") -> {"success":true} -[code] python3 hello.py -> "Hello, World!" -[text] -I have created a Python script named `hello.py` and successfully executed it. - -Here is the content of `hello.py`: -```python -print("Hello, World!") -``` -``` - -### Verbose (`--verbose` / `-v`) - -Optimized for automated parsing by agents. Steps are output as completed single-line JSON objects, followed by the final `{interaction}` metadata as a JSON line: - -```json -{"index":0,"type":"thought","status":"completed","thought":{"signature":"EvQBCvEBAQw5..."}} -{"index":1,"type":"function_call","status":"completed","function_call":{"name":"write_file","arguments":{"path":"hello.py","content":"print(\"Hello, World!\")"}}} -{"index":2,"type":"function_result","status":"completed","function_result":{"name":"write_file","result":{"success":true}}} -{"interaction":{"id":"v1_ChdIcjRp...","status":"completed","usage":{"total_tokens":9131,"total_input_tokens":8970,"total_output_tokens":161,"total_cached_tokens":0},"object":"interaction"}} -``` - -### JSON (`--json`) - -Raw streamed SSE events as JSONL (one raw event per line): - -```jsonl -{"event_type":"interaction.created","interaction":{...}} -{"index":0,"step":{"type":"thought"},"event_type":"step.start"} -{"index":0,"delta":{"signature":"EvQBC...","type":"thought_signature"},"event_type":"step.delta"} -{"event_type":"interaction.completed","interaction":{...}} -``` - -### Dry Run (`--dry-run`) - -Prints the equivalent `curl` command and exits without making an API call. - ---- - -## Multimodal I/O - -### Input - -```bash -gemini-api run "Describe this" --input image:photo.jpg -gemini-api run "Transcribe" --input audio:meeting.wav -gemini-api run "Summarize" --input document:report.pdf -``` - -### Output - -```bash -gemini-api run "Draw a cat" --model gemini-3-pro-image-preview --output cat.png -gemini-api run "Read aloud" --model gemini-3.1-flash-tts-preview --voice Kore --output speech.wav -``` - ---- - -## Multi-Turn Conversations - -```bash -# First turn -gemini-api run "Analyze the dataset" -# → interaction_id: int_abc123 - -# Second turn — continues the conversation -gemini-api run "Summarize in 3 bullets" --previous-interaction-id int_abc123 -``` - -For agent tests with environments: - -```bash -gemini-api agents test --prompt "Analyze data" -# → interaction_id: int_abc123 -# → environment: env_xyz789 - -gemini-api agents test --prompt "Now chart it" \ - --previous-interaction-id int_abc123 \ - --environment env_xyz789 -``` - ---- - -## Interaction Logging - -Every interaction is automatically logged to `.gemini/logs/.jsonl` in the current directory. Logs contain the request and reassembled response (SSE events combined into final content blocks). - -``` -.gemini/ -└── logs/ - └── int_abc123.jsonl -``` - -Each file has 2 lines: -- **Line 1:** Request (model, input, tools, system instruction) -- **Line 2:** Response (outputs, usage, status) - -Binary data (images, audio) is excluded from logs. - ---- - -## Troubleshooting - -### No API key - -``` -✗ No API key found. - - Try: - export GEMINI_API_KEY="your-api-key" - gemini-api run "Hello" --api-key "your-api-key" -``` - -### No agent.yaml - -``` -✗ No agent.yaml found in /home/user/project. - - Try: - gemini-api agents init my-agent - cd my-agent && gemini-api agents create -``` - -### Model not found - -``` -✗ API error (400): Model 'nonexistent' not found. - - Try: - gemini-api run "Hello" --model gemini-3-flash-preview -``` - -### Debug output - -Use `--verbose` to see request/response details: - -```bash -gemini-api run "Hello" --verbose -``` - -### Preview requests - -Use `--dry-run` to see the curl equivalent without making an API call: - -```bash -gemini-api agents create --dry-run -``` - ---- - -## Environment Variables - -| Variable | Description | -|---|---| -| `GEMINI_API_KEY` | API key for authentication | -| `GEMINI_API_BASE_URL` | Override API base URL | -| `AGENTS_WORKSPACE_PATH` | Target path prefix for inline files (default: `/.agents/`) | - ---- - -## Models - -| Model | Description | -|---|---| -| `gemini-3.5-flash` | Frontier + search (default) | -| `gemini-3.1-pro-preview` | SOTA reasoning + multimodal | -| `gemini-3-flash-preview` | Gemini 3 Flash Preview | -| `gemini-3.1-flash-lite` | Gemini 3.1 Flash-Lite | -| `gemini-2.5-flash` | Hybrid reasoning, 1M context | -| `gemini-2.5-pro` | SOTA coding + reasoning | -| `gemini-3-pro-image` | Nano Banana Pro (image generation) | -| `gemini-3.1-flash-image` | Nano Banana 2 (image generation) | -| `gemini-2.5-flash-image` | Native image generation | -| `gemini-3.1-flash-tts-preview` | Text-to-speech | -| `gemini-2.5-flash-preview-tts` | TTS | -| `gemini-2.5-pro-preview-tts` | TTS (pro) | -| `gemini-2.5-computer-use-preview-10-2025` | Computer use | -| `lyria-3-clip-preview` | Music: clip generation | -| `lyria-3-pro-preview` | Music: full-song | - -## Agents - -| Agent | Description | -|---|---| -| `deep-research-preview-04-2026` | Deep Research (latest) | -| `deep-research-max-preview-04-2026` | Deep Research Max | - ---- - -## License - -Apache-2.0 \ No newline at end of file diff --git a/LICENSE b/LICENSE index e8f908d..ece823a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,4 @@ - - Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -35,7 +34,8 @@ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work. + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the @@ -48,7 +48,7 @@ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally - submitted to the Licensor for inclusion in the Work by the copyright owner + submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent @@ -60,7 +60,7 @@ designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by the Licensor and + on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of @@ -106,7 +106,7 @@ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained - within such NOTICE file, excluding any notices that do not + within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or @@ -175,7 +175,18 @@ END OF TERMS AND CONDITIONS - Copyright 2026 Google LLC + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 9c357c3..ad08069 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,810 @@ -# Experimental Gemini API CLI +# gemini-api + +Command-line interface for the *Gemini* API. + +[![Built by Speakeasy](https://img.shields.io/badge/Built_by-SPEAKEASY-374151?style=for-the-badge&labelColor=f3f4f6)](https://www.speakeasy.com/?utm_source=google3-/third-party/gemini-api-cli&utm_campaign=cli) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + + +

+> [!IMPORTANT] +> This CLI is not yet ready for production use. Delete this notice before publishing to a package manager. + + +## Summary + +Gemini API: Use the Gemini Interactions API and managed-agent platform from the command line. + +Get started: + Set GEMINI_API_KEY, or run: gemini-api configure + Then run a model or managed agent: gemini-api agent --help + Add --dry-run to preview any API call without sending it. + + + +## Table of Contents + +* [gemini-api](#gemini-api) + * [CLI Installation](#cli-installation) + * [Shell Completion](#shell-completion) + * [CLI Example Usage](#cli-example-usage) + * [For AI agents](#for-ai-agents) + * [Authentication](#authentication) + * [Configuration](#configuration) + * [Commands](#commands) + * [Request Body Input](#request-body-input) + * [Server Selection](#server-selection) + * [Output Formats](#output-formats) + * [Server-Sent Event Streaming](#server-sent-event-streaming) + * [Pagination](#pagination) + * [Retries](#retries) + * [Error Handling](#error-handling) + * [Diagnostics](#diagnostics) +* [Development](#development) + * [Maturity](#maturity) + * [Contributions](#contributions) + + + + +## CLI Installation + +### Quick Install (Linux/macOS) -> An experimental CLI for the Gemini API. +```bash +curl -fsSL https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.sh | bash +``` + +### Quick Install (Windows PowerShell) -## Features +```powershell +iwr -useb https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.ps1 | iex +``` + +### Go Install -- **Model Interactions**: Run prompts against Gemini models with support for streaming, system instructions, and service tiers. -- **Multimodal Support**: Handle image understanding, image generation, and text-to-speech (TTS). -- **Agent Lifecycle**: Scaffold (`init`), deploy (`create`), test, and delete custom agents. -- **Deep Research**: Support for long-running Deep Research tasks with automatic polling and reconnection. -- **Tools**: Integrate tools like Code Execution and Google Search. -- **Environment Management**: Download snapshots of agent environments and pass custom sources (inline, github, gcs) to agents. +Alternatively, install directly via Go: -## Installation +```bash +go install google3/third_party/gemini_api_cli/cmd/gemini-api@latest +``` -### Via Install Script (Recommended) +### Manual Download -You can install the pre-compiled binary directly (no cloning required) using this single-line command: +Download pre-built binaries for your platform from the [releases page](https://github.com/google-gemini/gemini-api-cli/releases). + + + +## Shell Completion + +Shell completions are available for Bash, Zsh, Fish, and PowerShell. + +### Bash ```bash -curl -fsSL https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.sh | bash +# Add to ~/.bashrc: +source <(gemini-api completion bash) + +# Or install permanently: +gemini-api completion bash > /etc/bash_completion.d/gemini-api ``` -### From Source (via npm/Node.js) +### Zsh + +```zsh +# Add to ~/.zshrc: +source <(gemini-api completion zsh) -If you don't have Bun, you can install it as a Node.js package (requires Node.js ≥ 22): +# Or install permanently: +gemini-api completion zsh > "${fpath[1]}/_gemini-api" +``` + +### Fish + +```fish +gemini-api completion fish | source + +# Or install permanently: +gemini-api completion fish > ~/.config/fish/completions/gemini-api.fish +``` + +### PowerShell + +```powershell +gemini-api completion powershell | Out-String | Invoke-Expression +``` + + + +## CLI Example Usage + +### Quick start ```bash -git clone https://github.com/google-gemini/gemini-api-cli.git -cd gemini-api-cli -npm install -g . +# Run a managed agent by ID +gemini-api agent run "Analyze market trends for Q3" --agent deep-research-preview-04-2026 + +# Choose a different model +gemini-api generate "Write a haiku about APIs" --model gemini-2.5-pro + +# Generate an image (prints the written file path) +gemini-api image "a lighthouse at sunset" ``` -### Quick Start (for development) +### Example + +```bash +gemini-api environments list --api-key test_api_key --api-version v1beta + +``` + + + +## For AI agents + +This CLI is built to be driven by AI coding agents as well as people: everything an agent needs is discoverable from the binary itself, and every command can be validated without credentials. Work down this ladder: + +| Run | You get | +|-----|---------| +| `gemini-api --help`, `gemini-api agent list --help` | Commands by category, runnable examples, flags | +| `gemini-api --usage`, `gemini-api agent list --usage` | The command surface as machine-readable [KDL](https://kdl.dev): commands, aliases, flags, defaults, env vars, config keys | +| `gemini-api agent run --schema` | The exact JSON Schema of the command's request body (all `$ref`s bundled) — build a valid `--body` from it | +| `gemini-api agent list --dry-run` | The exact HTTP request (method, URL, headers, body), with no credentials or network call | +| `gemini-api agent list --output-format json` (or `--jq`) | Machine-readable output | + +### Discover the command surface + +```bash +# Every command, flag, default, env var and config key, as KDL +gemini-api --usage + +# One command's subtree only +gemini-api agent list --usage +``` + +### Read the exact request schema + +`--schema` is available on every command that accepts a request body (`--body`, stdin, or a whole-body flag where the command has one), including intent commands. It prints the JSON Schema the request is validated against and exits without calling the API. + +```bash +# JSON Schema (draft 2020-12) of the request body, with every $ref bundled under $defs +gemini-api agent run --schema +``` + +### Probe before you spend + +Start quota-spending commands with `--dry-run`. It validates inputs, resolves the request, redacts secrets and binary payloads, makes no network call, and exits 0. It never reads the OS keychain; credentials supplied by flag, environment, or config file are included only as `[REDACTED]`. + +```bash +# Human preview: the [DRY-RUN] block is on stderr and stdout is empty +gemini-api agent list --dry-run +gemini-api agent run "Analyze market trends for Q3" --agent deep-research-preview-04-2026 --dry-run + +# Machine preview: compact JSON on stdout and silent stderr +gemini-api agent list --dry-run --output-format json +``` + +The machine form writes one object per would-be request, one per line (NDJSON for multi-request commands), with exactly this shape: + +```json +{"dry_run":true,"request":{"method":"POST","url":"https://…","headers":{"Accept":["application/json"],…},"body":}} +``` + +`body` is a parsed JSON value when the body is JSON, a string for text, `""` for binary data, and `null` when absent. An explicit caller `--jq` also selects this JSON preview protocol, but the filter is not applied to preview objects. Command-declared jq presets do not select or filter the preview. + +Local mutation commands make no request under `--dry-run`: instead of a preview they emit one `{"dry_run":true,"local":true,"command":"…","message":"…"}` object. `select(.request)` keeps only would-be requests; `select(.local)` keeps the local no-ops. + +### Machine-readable output + +```bash +# JSON on stdout +gemini-api agent list --output-format json + +# Filter or reshape with a jq expression (always emits JSON, overrides --output-format) +gemini-api agent list --jq '.' + +# Print jq string results as plain text instead of JSON strings (like jq -r) +gemini-api agent list --jq '.' --raw-output +``` + +`--output-format toon` emits [TOON](https://github.com/toon-format/spec), a compact line-oriented format that uses fewer tokens than JSON; it is the default in agent mode. + +### Interactive mode +This CLI is non-interactive by default. Pass `--interactive` to prompt for missing inputs or open guided `configure` / `auth login` forms. Required-input prompts require an interactive terminal; off-TTY forms read line input from stdin. + +```bash +# Prompt for missing command inputs +gemini-api agent run --interactive + +# Open the guided configuration form +gemini-api configure --interactive + +# Explicitly launch the terminal command explorer +gemini-api explore +``` + +### Agent mode and structured errors +Agent mode turns on only when explicitly requested with `--agent-mode`; environment variables do not identify the caller. +In agent mode interactive prompts never launch, output defaults to TOON, and every failure — API errors and CLI usage errors alike — is one JSON envelope on stderr: +`--output-format json` and `--jq` use the same error envelope without requiring agent mode. + +```json +{ + "error": "...", + "error_type": "validation_error", + "error_reason": "CLI_VALIDATION", + "exit_code": 2, + "message": "human-readable message", + "hints": ["what to try next"] +} +``` + +`error_type` is one of `authentication_error`, `authorization_error`, `not_found`, `validation_error`, `rate_limit_error`, `server_error`, `api_error`, `connection_error`, `protocol_error`, `service_disabled`, `billing_disabled`, `runtime_error`, `unsupported_error`, `async_failed`, `async_timeout`, `async_unknown_state`. Classification reads the structured reason code at `$.details[*].reason`, then `$.status` in the error body (resolved against the nested `error` object when the body has one) before HTTP status, so a declared credential reason sent with HTTP 400 is not mistaken for request validation. `error_reason` carries the reason code found there, verbatim from a declared carrier when no declared rule matches it; it is absent for status-only API errors. Status-less local failures may use `CLI_VALIDATION`, `CLI_CONNECTION`, `CLI_PROTOCOL`, `CLI_RUNTIME`, `CLI_UNAVAILABLE`, `CLI_AUTHENTICATION`, or the async polling reasons `CLI_ASYNC_FAILED`, `CLI_ASYNC_TIMEOUT`, and `CLI_ASYNC_UNKNOWN_STATE`. `hints` preserves server guidance first, adds the most specific local taxonomy guidance, then typed CLI and command-specific guidance, removing exact duplicates. `exit_code` is always the code for the final `error_type` shown in the envelope: 1 runtime, 2 usage, or 3 authentication/authorization. + +### Lists, streams, and files + +List commands accept `--all` to fetch every page and stream results as they arrive (one JSON value per line with `--output-format json`; `--max-pages N` bounds the walk). + +Structured output and agent mode never write pagination hints to stderr; if a later page fails or the server repeats a cursor, the command exits non-zero after the pages already written. + +```bash +gemini-api agent list --all --output-format json +``` + +Streaming commands write each event as it arrives (one JSON object per line with `--output-format json`; a declared streamed projection prints just the selected text, e.g. `/data/delta/text`): + +```bash +gemini-api agent run "Analyze market trends for Q3" --agent deep-research-preview-04-2026 --stream --output-format json +``` + +Commands that produce media write the file and print only its path on stdout (`--out ` chooses the location, default `./gemini-image-{timestamp}-{rand}.{ext}`; `--raw-response` prints the API response instead): + +```bash +gemini-api image "a lighthouse at sunset" --out ./output/ +``` + +Long-running commands poll to a terminal response; human progress goes to stderr and machine-mode success keeps stderr silent. Add `--async` to `gemini-api video "a timelapse of a city at night" --async` to return its handle immediately, or tune foreground polling with `--poll-interval ` and `--poll-timeout `. Resume an escaped or timed-out operation with `gemini-api agent status --id `. + + + +## Authentication + +Authentication credentials can be configured in four ways (in order of priority): + +### 1. Command-line flags + +Pass credentials directly as flags to any command: + +```bash +gemini-api --api-key "$GEMINI_API_KEY" --access-token "$GEMINI_ACCESS_TOKEN" agent list +``` + +### 2. Environment variables + +Set credentials via environment variables: + +| Variable | Description | +|----------|-------------| +| `GEMINI_API_KEY` | Gemini API key sent as x-goog-api-key. | +| `GEMINI_ACCESS_TOKEN` | OAuth access token sent as a bearer Authorization header. | + +### 3. OS Keychain (recommended for workstations) + +Credentials are stored securely in your operating system's keychain when you run: + +```bash +gemini-api configure +``` + +Secret credentials (tokens, API keys, passwords) are automatically stored in: +- **macOS**: Keychain +- **Linux**: GNOME Keyring / KWallet (via D-Bus Secret Service) +- **Windows**: Windows Credential Locker + +If no keychain is available (e.g., in CI environments), credentials fall back to the config file. + +### 4. Configuration file + +Run the interactive `configure` command to store non-secret settings: + +```bash +gemini-api configure +``` + +Configuration is stored in `~/.config/gemini-api/config.yaml`. + + + +## Configuration + +`gemini-api configure` stores your settings in `~/.config/gemini-api/config.yaml`. You can run it interactively to set credentials and persistent preferences, or edit the config file directly. + +For authentication credentials specifically, see [Authentication](#authentication). + +### Global Parameters + +Certain parameters are configured globally and applied to all commands that use them. These parameters can be set via CLI flags, environment variables, or the config file. Individual commands can override global values with their own flags when needed. + +Priority: CLI flags > environment variables > config file + +| Source | Example | +|--------|---------| +| CLI flag | `gemini-api --api-version v1beta agent list` | +| Environment variable | `GEMINI_API_VERSION=v1beta gemini-api agent list` | +| Config file | `gemini-api configure` | + +#### Available Global Parameters + +| Flag | Type | Description | Environment | +| ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| `--api-version` | string | Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). | GEMINI_API_VERSION | +| `--api-revision` | string | Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. | GEMINI_API_REVISION | +| `--user-project` | string | Quota project header to send with Google GenAI API requests. | GEMINI_USER_PROJECT | + +### Example ```bash -# Install dependencies -bun install +# Set a global parameter via flag +gemini-api --api-version v1beta agent list -# Run in dev mode -bun run dev -- run "Hello" +# Or set via environment variable +GEMINI_API_VERSION=v1beta gemini-api agent list -# Build standalone binary for current platform -bun run compile -./dist/gemini-api --version +# Or configure globally (persisted to config file) +gemini-api configure ``` + + ## Commands +Commands are grouped the way `gemini-api --help` shows them. Every command accepts `--help`; body-bearing commands also accept `--schema` (exact request JSON Schema) and `--dry-run` (preview the request without sending it) — see [For AI agents](#for-ai-agents). + +### Create + +* [`generate`](docs/gemini-api_generate.md) - Text & multimodal generation (gemini-3.6-flash) + + ```bash + # Choose a different model + gemini-api generate "Write a haiku about APIs" --model gemini-2.5-pro + # Generate with the default model (streams the reply) + gemini-api generate "Explain concurrency in one sentence" + ``` + +* [`image`](docs/gemini-api_image.md) - Generate or edit images (gemini-3.1-flash-image) + + ```bash + # Generate an image (prints the written file path) + gemini-api image "a lighthouse at sunset" + # Write to a chosen path + gemini-api image "product shot, white bg" --out shots/hero.png + ``` + +* [`music`](docs/gemini-api_music.md) - Music generation (lyria-3-pro-preview) + + ```bash + # Generate a clip + gemini-api music "upbeat synthwave with a driving bassline" + ``` + +* [`tts`](docs/gemini-api_tts.md) - Text to speech (gemini-3.1-flash-tts-preview) — _not in this build_: "tts" is not yet callable through the Interactions API — the TTS models (gemini-3.1-flash-tts-preview) reject interaction requests and audio-modality speech needs the classic generateContent speech config. Verified live 2026-08-13 +* [`video`](docs/gemini-api_video.md) - Generate & edit video conversationally (gemini-omni-flash-preview) + + ```bash + # Return the interaction ID immediately; poll it yourself + gemini-api video "a timelapse of a city at night" --async + # Generate a video (polls until done, prints the written file path) + gemini-api video "a timelapse of a city at night" + ``` + +### Understand + +* [`analyze`](docs/gemini-api_analyze.md) - Ask questions about video, audio, PDF, or image files — _not in this build_: "analyze" is supported by the Interactions API (image, audio, document, and video content inputs) but needs the CLI's file-input adapter, which is not in this build yet. Meanwhile pass file content parts via "gemini-api agent run --body" +* [`embed`](docs/gemini-api_embed.md) - Vector embeddings (gemini-embedding-2) — _not in this build_: "embed" needs the classic GenAI API surface, which is not part of this interactions-only build +* [`tokens`](docs/gemini-api_tokens.md) - Count tokens without generating — _not in this build_: "tokens" needs the classic GenAI API surface, which is not part of this interactions-only build +* [`transcribe`](docs/gemini-api_transcribe.md) - Audio/video → text (timestamps, captions) — _not in this build_: "transcribe" is supported by the Interactions API (audio and video content inputs) but needs the CLI's file-input adapter, which is not in this build yet. Meanwhile pass file content parts via "gemini-api agent run --body" + +### Manage + +* [`agent`](docs/gemini-api_agent.md) - Run interactions with Gemini models or managed agents, and manage agent definitions + * [`run`](docs/gemini-api_agent_run.md) - Run an interaction with a Gemini model or a managed agent + + ```bash + # Run a managed agent by ID + gemini-api agent run "Analyze market trends for Q3" --agent deep-research-preview-04-2026 + # Start a background run, then poll with "agent status" + gemini-api agent run "Write a detailed research report on solar batteries" --background + # Run a model interaction (streams the reply) + gemini-api agent run "Explain the difference between concurrency and parallelism" --model gemini-3.6-flash + ``` + + * [`list`](docs/gemini-api_agent_list.md) - List managed agent definitions + * [`create`](docs/gemini-api_agent_create.md) - Create a managed agent definition + * [`delete`](docs/gemini-api_agent_delete.md) - Delete a managed agent definition by ID + * [`get`](docs/gemini-api_agent_get.md) - Get a managed agent definition by ID + * [`delete-interaction`](docs/gemini-api_agent_delete-interaction.md) - Delete an interaction by interaction ID + * [`status`](docs/gemini-api_agent_status.md) - Get status and output of an interaction by interaction ID + * [`cancel`](docs/gemini-api_agent_cancel.md) - Cancel an in-progress interaction by interaction ID +* [`batch`](docs/gemini-api_batch.md) - Async batch jobs at reduced cost — _not in this build_: "batch" needs the classic GenAI Batches API, which is not part of this interactions-only build +* [`configure`](docs/gemini-api_configure.md) - Configure authentication, global parameters, and preferences +* [`files`](docs/gemini-api_files.md) - Upload / list / download / delete media (48h TTL) + * [`list`](docs/gemini-api_files_list.md) - Lists the metadata for `File`s owned by the requesting project. + * [`delete`](docs/gemini-api_files_delete.md) - Deletes the `File`. + * [`get`](docs/gemini-api_files_get.md) - Gets the metadata for the given `File`. + * [`register`](docs/gemini-api_files_register.md) - Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails. +* [`models`](docs/gemini-api_models.md) - Full model operations — list and get model metadata, embed, count tokens, and generate with complete request control + * [`list`](docs/gemini-api_models_list.md) - Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API. + * [`get`](docs/gemini-api_models_get.md) - Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information. + +### Advanced + +* [`docs`](docs/gemini-api_docs.md) - Gemini API documentation & guides — _not in this build_: "docs" curated guides are not part of this build yet. Meanwhile browse https://ai.google.dev/gemini-api/docs +* [`triggers`](docs/gemini-api_triggers.md) - Schedule and manage cron triggers that run managed agents + * [`list`](docs/gemini-api_triggers_list.md) - List triggers for a project + * [`delete`](docs/gemini-api_triggers_delete.md) - Delete a trigger by ID + * [`get`](docs/gemini-api_triggers_get.md) - Get a trigger by ID + * [`update`](docs/gemini-api_triggers_update.md) - Update a trigger by ID + * [`list-executions`](docs/gemini-api_triggers_list-executions.md) - List executions for a trigger + * [`run`](docs/gemini-api_triggers_run.md) - Run a trigger immediately +* [`webhooks`](docs/gemini-api_webhooks.md) - Manage webhook endpoints and signing secrets for event delivery + * [`list`](docs/gemini-api_webhooks_list.md) - List webhook endpoints + * [`create`](docs/gemini-api_webhooks_create.md) - Create a webhook endpoint + * [`delete`](docs/gemini-api_webhooks_delete.md) - Delete a webhook by ID + * [`get`](docs/gemini-api_webhooks_get.md) - Get a webhook by ID + * [`update`](docs/gemini-api_webhooks_update.md) - Update a webhook by ID + * [`ping`](docs/gemini-api_webhooks_ping.md) - Send a ping event to a webhook + * [`rotate-signing-secret`](docs/gemini-api_webhooks_rotate-signing-secret.md) - Rotate the signing secret for a webhook + +### Additional commands + +* [`environments`](docs/gemini-api_environments.md) - Operations for environments + * [`list`](docs/gemini-api_environments_list.md) - Lists environments. + * [`create`](docs/gemini-api_environments_create.md) - Creates an environment. + * [`delete`](docs/gemini-api_environments_delete.md) - Deletes an environment. + * [`get`](docs/gemini-api_environments_get.md) - Gets an environment. + * [`internal`](docs/gemini-api_environments_internal.md) - Operations for internal + * [`start-upload`](docs/gemini-api_environments_internal_start-upload.md) - Start an environment file upload + * [`files`](docs/gemini-api_environments_files.md) - Operations for files + * [`list`](docs/gemini-api_environments_files_list.md) - Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper. +* [`credentials`](docs/gemini-api_credentials.md) - Operations for credentials + * [`list`](docs/gemini-api_credentials_list.md) - Lists credentials for a project. + * [`create`](docs/gemini-api_credentials_create.md) - Creates a credential. + * [`delete`](docs/gemini-api_credentials_delete.md) - Deletes a credential. Fails if referenced by active triggers. + * [`get`](docs/gemini-api_credentials_get.md) - Gets metadata of a single credential (no secret fields). + * [`update`](docs/gemini-api_credentials_update.md) - Updates a credential. + + + +## Request Body Input + +Commands that accept a request body take it three ways, with a clear priority chain. The examples use `gemini-api environments create`; every body-bearing command works the same way and prints its exact request schema with `--schema`. + +### `--body` flag + +Provide the entire request body as a JSON string: + ```bash -# Run an interaction against a model -gemini-api run "What is the capital of France?" +gemini-api environments create --body '{"network":{"allowlist":[{"domain":"github.com","transform":[{"Authorization":"Bearer your-token"}]},{"domain":"*.googleapis.com"}]}}' +``` + +### Stdin piping (lowest priority) -# Scaffold, test, and deploy agents -gemini-api agents init my-agent -gemini-api agents test --prompt "Hello" -gemini-api agents create +Pipe JSON into any command that accepts a request body: -# Manage environment files -gemini-api files download +```bash +echo '{"network":{"allowlist":[{"domain":"github.com","transform":[{"Authorization":"Bearer your-token"}]},{"domain":"*.googleapis.com"}]}}' | gemini-api environments create ``` -## E2E Tests +This is useful for chaining commands, reading from files, or scripting: + +```bash +# Read body from a file +gemini-api environments create < request.json + +# Pipe from another command +curl -s https://example.com/request.json | gemini-api environments create +``` + +### Priority + +When multiple input methods are used, the priority is: -We have a suite of End-to-End (E2E) tests located in `tests/e2e/`. -These scripts run both the `--dry-run` variant (to show the command) and the live variant (to show the result). +| Priority | Source | Description | +|----------|--------|-------------| +| 1 (highest) | Individual flags | A field flag always wins | +| 2 | `--body` flag | Whole-body JSON via flag | +| 3 (lowest) | Stdin | Piped JSON input | + + + +## Server Selection + +### Override Server URL + +Use `--server-url` to override the server URL entirely: + +```bash +gemini-api --server-url https://custom-api.example.com agent list +``` + +**Precedence**: `--server-url` > default + + + +## Output Formats + +Every command supports a `--output-format` flag that controls how the response is rendered to stdout. + +### Available formats + +| Format | Flag | Description | +|--------|------|-------------| +| Pretty | `--output-format pretty` (default) | Aligned key-value pairs with color, nested indentation. Human-readable at a glance. | +| JSON | `--output-format json` | JSON output. Passthrough when the response is already JSON (preserves original field order and numeric precision). Falls back to typed marshaling otherwise. | +| YAML | `--output-format yaml` | YAML output via standard marshaling. | +| Table | `--output-format table` | Tabular output for array responses. | +| TOON | `--output-format toon` | [Token-Oriented Object Notation](https://github.com/toon-format/spec) — a compact, line-oriented format that typically uses 30–60% fewer tokens than JSON. Well-suited for piping responses into LLM prompts. | -To run a specific test: ```bash -bash tests/e2e/cuj_01.sh +# Default pretty output +gemini-api agent list + +# Machine-readable JSON +gemini-api agent list --output-format json + +# TOON for LLM-friendly compact output +gemini-api agent list --output-format toon + +# Pipe JSON to jq without using --output-format +gemini-api agent list --output-format json | jq '.' +``` + +### jq filtering + +Use `--jq` to filter or transform the response inline using a [jq](https://jqlang.org) expression. This always outputs JSON and overrides `--output-format`: + +```bash +# Extract a single field +gemini-api agent list --jq '.' + +# Reshape with any jq program; --raw-output prints string results as plain text (like jq -r) +gemini-api agent list --jq '.' --raw-output +``` + +### Color control + +Use `--color` to control terminal colors: + +| Value | Behavior | +|-------|----------| +| `auto` (default) | Color when stdout is a TTY, plain text otherwise | +| `always` | Always colorize | +| `never` | Never colorize | + +The `NO_COLOR` and `FORCE_COLOR` environment variables are also respected. + +### Streaming and pagination + +When using `--all` (pagination) or streaming operations, output is written incrementally as items arrive: + +| Format | Streaming behavior | +|--------|-------------------| +| `json` | One compact JSON object per line ([NDJSON](https://github.com/ndjson/ndjson-spec)) | +| `yaml` | YAML documents separated by `---` | +| `toon` | One TOON-encoded object per block, separated by blank lines | +| `pretty` (default) | Pretty-printed items separated by blank lines | + + + +## Server-Sent Event Streaming + +Some operations return server-sent events (SSE). These are streamed to the terminal in real-time, with each event output as a separate JSON object (one per line). + +```bash +# Stream events in JSON format +gemini-api agent run "Analyze market trends for Q3" --agent deep-research-preview-04-2026 --stream --output-format json + +# Filter streaming events with jq +gemini-api agent run "Analyze market trends for Q3" --agent deep-research-preview-04-2026 --stream --output-format json --jq '.' +``` + +Events are output as they arrive. Use `Ctrl+C` to stop streaming. + +For operation commands with a declared streamed projection, the selected string is written raw as it arrives. When the command exposes a stream toggle flag, its default decides the response shape — the command's help says whether to pass `--stream=false` for one complete JSON response (streaming on by default) or `--stream` to request a streamed response (off by default). Use `-o json` to keep each full streamed event. + + + +## Pagination + +Some operations in this CLI support automatic pagination. These operations accept `--all` to automatically fetch all pages and stream results incrementally. + +### Basic usage + +```bash +# Fetch a single page (default behavior) +gemini-api agent list + +# Automatically fetch all pages +gemini-api agent list --all ``` -See [E2E.md](./tests/e2e/E2E.md) for a full list of Critical User Journeys and their status. +### Limiting pages -## Skills +Use `--max-pages` with `--all` to cap the number of pages fetched. A negative value is invalid; `0` means unlimited. Passing `--max-pages` without `--all` is an error. -This repository includes a skill for AI agents to understand how to use this CLI. +```bash +# Fetch at most 5 pages +gemini-api agent list --all --max-pages 5 +``` + +### Output formats + +When using `--all`, output is streamed as each page is fetched. Operations whose pagination declaration names an `outputs.results` array emit one item at a time. Other operations emit one complete page object at a time, preserving the single-page response shape and any continuation cursor. + +| Format | Behavior | +|--------|----------| +| `--output-format json` | One JSON object per line ([NDJSON](https://github.com/ndjson/ndjson-spec)) | +| `--output-format yaml` | YAML documents separated by `---` | +| `--output-format toon` | One TOON-encoded block per item, separated by blank lines | +| Default (pretty) | Pretty-printed items separated by blank lines | + +```bash +# Stream all results as NDJSON +gemini-api agent list --all --output-format json + +# Pipe to jq for further processing +gemini-api agent list --all --output-format json | jq '.' + +# Use the built-in --jq flag +gemini-api agent list --all --jq '.' +``` + +### How it works + +Under the hood, `--all` calls the operation once, then follows the underlying `Next()` pagination closure to fetch subsequent pages. Results are written to stdout as they arrive rather than buffered in memory, so this works well even with large result sets. + +Without `--all`, paginated operations behave like any other command — pass cursor, page, offset, or limit flags manually and get a single page of results. In pretty or table output, a cursor response that proves another page exists prints a hint on stderr. JSON, YAML, TOON, `--jq`, and agent mode keep stderr silent on success. Offset/limit responses do not guess from a full result page. Cursor operations that declare both a results array and a mutable limit also suppress the hint because the client cannot safely reproduce the SDK's runtime limit check. + +Pagination can fail after earlier pages have already been written. A later-page API failure or a repeated/cyclic continuation cursor stops with a non-zero exit status; callers should treat stdout as partial whenever the command exits non-zero. `--all` tracks cursor values and stops before issuing another request when the server repeats one; the same applies to next URLs when the target generator supports them. + + + +## Retries + +Some operations in this CLI support automatic retries with exponential backoff. + +### Configure retries + +Retry flags are supported but intentionally omitted from `--help`. For persistent agent configuration, use `~/.config/gemini-api/config.yaml`: + +```yaml +timeout: 30s +no_retries: false +retry_connection_errors: true +retry_max_elapsed_time: 1m +# retry_config replaces the whole policy (overrides retry_max_elapsed_time): +# retry_config: '{"strategy":"backoff","backoff":{"initialInterval":500,"maxInterval":60000,"exponent":1.5,"maxElapsedTime":300000}}' +``` + +The equivalent hidden flags are `--no-retries`, `--retry-config`, `--retry-connection-errors`, and `--retry-max-elapsed-time`. + +### Retry-After + +`Retry-After` (integer seconds or an RFC1123 date) and `retry-after-ms` override the next computed interval. With the `backoff` strategy, a server-directed wait that exceeds the remaining `maxElapsedTime` budget is not slept; the last response is returned. With `attempt-count-backoff`, `maxRetries` bounds attempts, while `timeout` bounds wall-clock time; `maxElapsedTime` does not apply. + +### Timeout + +`timeout` and `--timeout` bound the whole operation, including retry sleeps: + +```bash +gemini-api agent list --timeout 30s +``` + +**Precedence**: `--no-retries` > `--retry-config` > individual flags > config file > API specification defaults. + + + +## Error Handling + +The CLI uses standard exit codes to indicate success or failure: + +| Exit Code | Meaning | +|-----------|---------| +| `0` | Success | +| `1` | Runtime/API failure | +| `2` | Usage or input failure | +| `3` | Authentication or authorization failure | + +On success, the response data is printed to **stdout** as JSON. On failure, error details are printed to **stderr**. + +```bash +# Capture output and handle errors +gemini-api agent list --output-format json > output.json 2> error.log +if [ $? -ne 0 ]; then + echo "Error occurred, see error.log" +fi +``` +In pretty mode, each error is printed once as `Error (): `, followed by its reason/HTTP status, actionable `Fix:` bullets, and only non-duplicative residual details. + +In agent mode, or with explicit `--output-format json`, `--output-format toon`, or `--jq` machine output, stderr is one classified JSON envelope with `exit_code`, `error_type`, optional `error_reason`, `message`, `hints`, and optional `status_code` — see [For AI agents](#for-ai-agents). + +`error_reason` is the structured reason code read from the error body at `$.details[*].reason`, then `$.status` (resolved against the nested `error` object when the body has one). + + + +## Diagnostics + +The CLI includes two diagnostic flags available on all commands: + +### Dry Run + +Preview what would be sent without making any network calls: + +```bash +gemini-api agent list --dry-run +``` + +In human output modes, stdout is empty and the `[DRY-RUN]` block goes to stderr. It includes: +- HTTP method and URL +- Request headers (sensitive values redacted) +- Request body preview (sensitive fields redacted) + +With `--output-format json`, or with a caller-explicit `--jq`, stderr is silent and stdout is NDJSON: one compact preview object per would-be request. The jq filter is not applied, and command-declared jq presets do not select the JSON protocol. + +```json +{"dry_run":true,"request":{"method":"POST","url":"https://…","headers":{"Accept":["application/json"],…},"body":}} +``` + +JSON bodies remain structured; text bodies are strings; binary bodies are `""`; absent bodies are `null`. Headers retain all values as arrays, with credentials replaced by `[REDACTED]`. Dry-run never reads the OS keychain, but credentials supplied by flag, environment, or config file still appear redacted. The command exits successfully without contacting the API. + +Local mutation commands emit one `{"dry_run":true,"local":true,"command":"…","message":"…"}` object in place of a preview; filter with `select(.request)` or `select(.local)`. + +### Debug + +Log request and response diagnostics while running normally: + +```bash +gemini-api agent list --debug +``` -- [Gemini API CLI Skill](./skills/gemini-api-cli/SKILL.md) — Comprehensive guide for agents, covering: - - Normal model calls (text, multi-turn, tools) - - Agent lifecycle management (init, create, test, delete) - - Agent calls (invoking antigravity-preview-05-2026 and Deep Research) - - Genmedia (image generation, editing, TTS, music) +Debug output goes to stderr and includes: +- Request method, URL, headers, and body preview +- Response status, headers, and body preview +- Transport errors (if any) +The command still executes normally and produces its regular output on stdout. +### Flag Precedence -## Documentation +If both `--dry-run` and `--debug` are set, `--dry-run` takes precedence and no network calls are made. -- [DOCS.md](./DOCS.md) — Full user guide and reference -- [tests/e2e/E2E.md](./tests/e2e/E2E.md) — End-to-End Test Plan +### Security +Sensitive information is automatically redacted in diagnostic output: +- **Headers**: `Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`, and other security headers show `[REDACTED]` +- **Body**: JSON fields named `password`, `secret`, `token`, `api_key`, `client_secret`, etc. show `[REDACTED]` +- **Binary data**: binary media and canonical base64 strings are replaced with `` +- **URL query**: credential-like query parameters are replaced with `[REDACTED]` -## Releasing New Versions +Diagnostic output should still be treated as potentially sensitive operational data. + -To release a new version and make the binaries available via the install script: + -1. Update the version in `package.json` and `src/cli.ts`. -2. Commit and push the changes. -3. Create a new Release on GitHub with a tag matching `v*` (e.g., `v0.2.0`). -4. The **Release Binaries** GitHub workflow will automatically trigger, build the standalone binaries for all supported platforms, and upload them as assets to the release. +# Development -## Licensing & Disclaimer +## Maturity -Copyright 2026 Google LLC +This CLI is in beta, and there may be breaking changes between versions without a major version update. Therefore, we recommend pinning usage +to a specific package version. This way, you can install the same version each time without breaking changes unless you are intentionally +looking for the latest version. -All software is licensed under the Apache License, Version 2.0 (Apache 2.0); you may not use this file except in compliance with the Apache 2.0 license. You may obtain a copy of the Apache 2.0 license at: [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) +## Contributions -All other materials are licensed under the Creative Commons Attribution 4.0 International License (CC-BY). You may obtain a copy of the CC-BY license at: [https://creativecommons.org/licenses/by/4.0/legalcode](https://creativecommons.org/licenses/by/4.0/legalcode) +This CLI is generated programmatically. Edits to generated files are overwritten on regeneration. To customize it: -Unless required by applicable law or agreed to in writing, all software and materials distributed here under the Apache 2.0 or CC-BY licenses are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the licenses for the specific language governing permissions and limitations under those licenses. +- **Configuration and behavior:** Use [OpenAPI overlays](https://www.speakeasy.com/docs/prep-openapi/overlays/create-overlays) in the Speakeasy workflow with `x-speakeasy-*` extensions (for example, `x-speakeasy-cli-commands`) to define commands, flags, help text, examples, authentication, and grouping. +- **Persistent code changes:** Store unified diffs as [patch files](https://www.speakeasy.com/docs/sdks/customize/code/patch-files/patch-files) at `.speakeasy/patches/.patch`; they are re-applied on every generation. +- **Hand-written commands:** Add them under `internal/cli/custom/`; the scaffold is generated once and never overwritten. -This is not an official Google product. +### CLI Created by [Speakeasy](https://www.speakeasy.com/?utm_source=google3-/third-party/gemini-api-cli&utm_campaign=cli) diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..3ea5d42 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,6 @@ + +```bash +gemini-api environments list --api-key test_api_key --api-version v1beta + +``` + \ No newline at end of file diff --git a/biome.json b/biome.json deleted file mode 100644 index ed19487..0000000 --- a/biome.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json", - "assist": { "actions": { "source": { "organizeImports": "on" } } }, - "overrides": [ - { - "includes": ["dist/**", "node_modules/**"], - "linter": { "enabled": false }, - "formatter": { "enabled": false } - } - ], - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "always", - "trailingCommas": "all" - } - } -} diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 7b9dbf0..0000000 --- a/bun.lock +++ /dev/null @@ -1,117 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "@google/gemini-api-cli", - "dependencies": { - "citty": "^0.1.6", - "dotenv": "^17.4.2", - "js-yaml": "^4.1.0", - "zod": "^3.24.0", - }, - "devDependencies": { - "@biomejs/biome": "latest", - "@types/js-yaml": "^4.0.9", - "@types/node": "^22.0.0", - "bun-types": "latest", - "esbuild": "latest", - "typescript": "^5.7.0", - }, - }, - }, - "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.13", "@biomejs/cli-darwin-x64": "2.4.13", "@biomejs/cli-linux-arm64": "2.4.13", "@biomejs/cli-linux-arm64-musl": "2.4.13", "@biomejs/cli-linux-x64": "2.4.13", "@biomejs/cli-linux-x64-musl": "2.4.13", "@biomejs/cli-win32-arm64": "2.4.13", "@biomejs/cli-win32-x64": "2.4.13" }, "bin": { "biome": "bin/biome" } }, "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.13", "", { "os": "win32", "cpu": "x64" }, "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], - - "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], - - "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - - "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - } -} diff --git a/cmd/gemini-api/main.go b/cmd/gemini-api/main.go new file mode 100644 index 0000000..9303636 --- /dev/null +++ b/cmd/gemini-api/main.go @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/google-gemini/gemini-api-cli/internal/cli" + "github.com/google-gemini/gemini-api-cli/internal/clierrors" +) + +// version and buildTime can be set at build time using Go linker flags: +// +// go build -ldflags "-X main.version=x.y.z -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" ./cmd/gemini-api +var version string +var buildTime string + +func main() { + if version != "" { + cli.Version = version + } + if buildTime != "" { + cli.BuildTime = buildTime + } + + if err := cli.Execute(); err != nil { + var rendered interface{ Rendered() bool } + if !errors.As(err, &rendered) || !rendered.Rendered() { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(clierrors.ExitCode(err)) + } +} diff --git a/cmd/gendocs/main.go b/cmd/gendocs/main.go new file mode 100644 index 0000000..0865c2f --- /dev/null +++ b/cmd/gendocs/main.go @@ -0,0 +1,220 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package main + +import ( + "bytes" + "crypto/sha256" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/cli" + "github.com/google-gemini/gemini-api-cli/internal/clierrors" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +const ( + usage = "usage: gendocs [output-dir]" + usageDescription = "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs)." +) + +var usageLines = []string{usage, usageDescription} + +func main() { + dir, showHelp, exitCode, messages := parseArgs(os.Args[1:]) + for _, message := range messages { + fmt.Fprintln(os.Stderr, message) + } + if exitCode != 0 { + os.Exit(exitCode) + } + if showHelp { + return + } + + if err := os.MkdirAll(dir, 0755); err != nil { + log.Fatal(err) + } + + rootCmd, err := cli.NewRootCommand() + if err != nil { + log.Fatal(err) + } + + // Exclude cobra's builtin help and completion commands from the generated + // docs. A generated command may legitimately be named "help" or + // "completion" (via name overrides); such commands always carry a + // speakeasy_operation or speakeasy_cli_group annotation and must be kept. + for _, c := range rootCmd.Commands() { + if c.Name() != "help" && c.Name() != "completion" { + continue + } + if c.Annotations["speakeasy_operation"] == "" && c.Annotations["speakeasy_cli_group"] == "" { + rootCmd.RemoveCommand(c) + } + } + + // Truncate any command names that would cause filenames to exceed OS limits. + // Cobra doc generates filenames from the full command path joined with "_". + truncateLongCommandNames(rootCmd, 0) + + if err := genMarkdownTreeNoDate(rootCmd, dir); err != nil { + log.Fatal(err) + } +} + +func parseArgs(args []string) (dir string, showHelp bool, exitCode int, messages []string) { + for _, arg := range args { + if arg == "-h" || arg == "--help" || arg == "help" { + return "", true, 0, usageLines + } + } + + if len(args) == 0 { + return "./docs", false, 0, nil + } + + if len(args) == 1 { + arg := args[0] + if strings.HasPrefix(arg, "-") { + return "", false, 2, append([]string{fmt.Sprintf("unknown flag %q", arg)}, usageLines...) + } + return arg, false, 0, nil + } + + for _, arg := range args { + if strings.HasPrefix(arg, "-") { + return "", false, 2, append([]string{fmt.Sprintf("unknown flag %q", arg)}, usageLines...) + } + } + + reason := "too many arguments: " + strings.Join(args, " ") + return "", false, 2, append([]string{reason}, usageLines...) +} + +// truncateLongCommandNames walks the command tree and truncates any Use fields +// that would cause the generated doc filename to exceed filesystem limits. +// Cobra constructs filenames as: parent_path + "_" + use + ".md" +func truncateLongCommandNames(cmd *cobra.Command, parentPathLen int) { + const maxFilenameLen = 240 // well under the 255 limit + use := strings.SplitN(cmd.Use, " ", 2)[0] + + // Calculate what the filename length would be: parentPath_use.md + nameLen := parentPathLen + len(use) + len(".md") + if parentPathLen > 0 { + nameLen++ // for the "_" separator + } + + if nameLen > maxFilenameLen && len(use) > 16 { + available := maxFilenameLen - parentPathLen - len(".md") - 10 // 10 for _hash + if parentPathLen > 0 { + available-- // for separator + } + if available < 8 { + available = 8 + } + hash := sha256.Sum256([]byte(use)) + truncated := use[:available] + fmt.Sprintf("_%x", hash[:4]) + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + cmd.Annotations["speakeasy_original_name"] = use + if parts := strings.SplitN(cmd.Use, " ", 2); len(parts) > 1 { + cmd.Use = truncated + " " + parts[1] + } else { + cmd.Use = truncated + } + use = truncated + } + + childPathLen := parentPathLen + len(use) + if parentPathLen > 0 { + childPathLen++ + } + + for _, child := range cmd.Commands() { + truncateLongCommandNames(child, childPathLen) + } +} + +// genMarkdownTreeNoDate generates markdown docs for all commands without the +// "Auto generated by spf13/cobra on " footer that cobra adds, which +// causes unnecessary churn in version control. +func genMarkdownTreeNoDate(cmd *cobra.Command, dir string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := genMarkdownTreeNoDate(c, dir); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".md" + filename := filepath.Join(dir, basename) + + var buf bytes.Buffer + if err := doc.GenMarkdownCustom(cmd, &buf, func(s string) string { return s }); err != nil { + return err + } + + content := buf.String() + if idx := strings.LastIndex(content, "\n###### Auto generated"); idx != -1 { + content = strings.TrimRight(content[:idx], "\n") + "\n" + } + content += machineInterfaceFooter(cmd) + + return os.WriteFile(filename, []byte(content), 0644) +} + +func commandRuntimePath(cmd *cobra.Command) string { + var parts []string + for cur := cmd; cur != nil; cur = cur.Parent() { + name := cur.Name() + if orig, ok := cur.Annotations["speakeasy_original_name"]; ok && orig != "" { + name = orig + } + parts = append([]string{name}, parts...) + } + return strings.Join(parts, " ") +} + +func machineInterfaceFooter(cmd *cobra.Command) string { + var b strings.Builder + if _, ok := cmd.Annotations["speakeasy_operation"]; ok { + path := commandRuntimePath(cmd) + b.WriteString("\n### Machine interface\n\n") + fmt.Fprintf(&b, "* `%s --usage` — this command's flags, defaults and env vars as machine-readable KDL\n", path) + if cmd.Flags().Lookup("schema") != nil { + fmt.Fprintf(&b, "* `%s --schema` — the exact JSON Schema of the request body (all `$ref`s bundled)\n", path) + } + fmt.Fprintf(&b, "* `%s --dry-run` — preview the request without OS-keychain access or a network call (human preview on stderr)\n", path) + b.WriteString("* `--dry-run --output-format json` (or a caller-explicit `--jq`) writes one preview object per request as NDJSON on stdout; jq is not applied to previews\n") + if cmd.Flags().Lookup("output-file") != nil { + b.WriteString("* `--output-format json` or `--jq ` for machine-readable live output when the response is JSON; a binary response body is written raw to stdout (use `--output-file ` or `--output-b64`); in agent mode errors are a JSON envelope on stderr\n") + } else { + b.WriteString("* `--output-format json` or `--jq ` for machine-readable live output; in agent mode errors are a JSON envelope on stderr\n") + } + } + fmt.Fprintln(&b) + fmt.Fprintln(&b, clierrors.HelpFooter) + return b.String() +} diff --git a/cmd/gendocs/main_test.go b/cmd/gendocs/main_test.go new file mode 100644 index 0000000..96c0e97 --- /dev/null +++ b/cmd/gendocs/main_test.go @@ -0,0 +1,216 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package main + +import ( + "reflect" + "strings" + "testing" + + "github.com/google-gemini/gemini-api-cli/internal/clierrors" + "github.com/spf13/cobra" +) + +func TestParseArgs(t *testing.T) { + tests := []struct { + name string + args []string + wantDir string + wantShowHelp bool + wantExitCode int + wantMessages []string + wantPrefix string + }{ + { + name: "short help", + args: []string{"-h"}, + wantShowHelp: true, + wantExitCode: 0, + wantMessages: []string{ + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "long help", + args: []string{"--help"}, + wantShowHelp: true, + wantExitCode: 0, + wantMessages: []string{ + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "help command", + args: []string{"help"}, + wantShowHelp: true, + wantExitCode: 0, + wantMessages: []string{ + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "help after output directory", + args: []string{"./out", "-h"}, + wantShowHelp: true, + wantExitCode: 0, + wantMessages: []string{ + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "help before output directory", + args: []string{"-h", "./out"}, + wantShowHelp: true, + wantExitCode: 0, + wantMessages: []string{ + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "unknown long flag", + args: []string{"--bogus"}, + wantExitCode: 2, + wantMessages: []string{ + `unknown flag "--bogus"`, + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "unknown short flag", + args: []string{"-x"}, + wantExitCode: 2, + wantMessages: []string{ + `unknown flag "-x"`, + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "double dash is an unknown flag", + args: []string{"--"}, + wantExitCode: 2, + wantMessages: []string{ + `unknown flag "--"`, + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "single dash is an unknown flag", + args: []string{"-"}, + wantExitCode: 2, + wantMessages: []string{ + `unknown flag "-"`, + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + }, + }, + { + name: "no arguments", + wantDir: "./docs", + wantExitCode: 0, + }, + { + name: "output directory", + args: []string{"/some/dir"}, + wantDir: "/some/dir", + wantExitCode: 0, + }, + { + name: "too many directories", + args: []string{"first", "second"}, + wantExitCode: 2, + wantPrefix: "too many arguments:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir, showHelp, exitCode, messages := parseArgs(tt.args) + if dir != tt.wantDir { + t.Errorf("parseArgs() dir = %q, want %q", dir, tt.wantDir) + } + if showHelp != tt.wantShowHelp { + t.Errorf("parseArgs() showHelp = %t, want %t", showHelp, tt.wantShowHelp) + } + if exitCode != tt.wantExitCode { + t.Errorf("parseArgs() exitCode = %d, want %d", exitCode, tt.wantExitCode) + } + if tt.wantPrefix != "" { + if len(messages) == 0 || !strings.HasPrefix(messages[0], tt.wantPrefix) { + t.Errorf("parseArgs() reason = %q, want prefix %q", messages, tt.wantPrefix) + } + wantUsage := []string{ + "usage: gendocs [output-dir]", + "Writes the CLI's Cobra markdown command docs into output-dir (default ./docs).", + } + if len(messages) < 1 || !reflect.DeepEqual(messages[1:], wantUsage) { + t.Errorf("parseArgs() usage messages = %q, want %q", messages, wantUsage) + } + } else if !reflect.DeepEqual(messages, tt.wantMessages) { + t.Errorf("parseArgs() messages = %q, want %q", messages, tt.wantMessages) + } + }) + } +} + +func TestMachineInterfaceFooter(t *testing.T) { + root := &cobra.Command{Use: "petstore"} + jsonCmd := &cobra.Command{Use: "list", Annotations: map[string]string{"speakeasy_operation": "listPets"}} + binaryCmd := &cobra.Command{Use: "download", Annotations: map[string]string{"speakeasy_operation": "downloadPet"}} + binaryCmd.Flags().String("output-file", "", "") + binaryCmd.Flags().Bool("output-b64", false, "") + local := &cobra.Command{Use: "version"} + root.AddCommand(jsonCmd, binaryCmd, local) + + jsonFooter := machineInterfaceFooter(jsonCmd) + for _, want := range []string{ + "### Machine interface", + "* `petstore list --usage`", + "* `petstore list --dry-run`", + "* `--output-format json` or `--jq ` for machine-readable live output; in agent mode errors are a JSON envelope on stderr\n", + } { + if !strings.Contains(jsonFooter, want) { + t.Errorf("json footer missing %q:\n%s", want, jsonFooter) + } + } + if strings.Contains(jsonFooter, "--output-file") { + t.Errorf("json footer must not advertise binary output flags:\n%s", jsonFooter) + } + + binaryFooter := machineInterfaceFooter(binaryCmd) + want := "* `--output-format json` or `--jq ` for machine-readable live output when the response is JSON; a binary response body is written raw to stdout (use `--output-file ` or `--output-b64`); in agent mode errors are a JSON envelope on stderr\n" + if !strings.Contains(binaryFooter, want) { + t.Errorf("binary footer missing %q:\n%s", want, binaryFooter) + } + + localFooter := machineInterfaceFooter(local) + if strings.Contains(localFooter, "Machine interface") { + t.Errorf("local command footer must not carry the machine interface block:\n%s", localFooter) + } + for name, footer := range map[string]string{"json": jsonFooter, "binary": binaryFooter, "local": localFooter} { + if !strings.HasSuffix(footer, clierrors.HelpFooter+"\n") { + t.Errorf("%s footer must end with the exit-code line:\n%s", name, footer) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3685a57 --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module github.com/google-gemini/gemini-api-cli + +go 1.25.10 + +require ( + github.com/alpkeskin/gotoon v0.1.1 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.3.4 + github.com/charmbracelet/huh v0.6.0 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/itchyny/gojq v0.12.18 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.9 + github.com/spyzhov/ajson v0.9.6 + github.com/zalando/go-keyring v0.2.6 + golang.org/x/term v0.40.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/timefmt-go v0.1.7 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b14a77f --- /dev/null +++ b/go.sum @@ -0,0 +1,108 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/alpkeskin/gotoon v0.1.1 h1:GQOVwMfWKINnfEA6slrXHJaJYDwnUFmrPlXOtnuja1w= +github.com/alpkeskin/gotoon v0.1.1/go.mod h1:XRTz8RM4tz8M2nB37MNRN8rHF4YgeYd8nIXmoU0B0+M= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= +github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= +github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= +github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= +github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/agent/cancel.go b/internal/cli/agent/cancel.go new file mode 100644 index 0000000..0568b90 --- /dev/null +++ b/internal/cli/agent/cancel.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var cancelCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "The unique identifier of the interaction to cancel. [required]"}, +} + +// initCancelCmd initializes the cancel command. +func initCancelCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "cancel", + Short: "Cancel an in-progress interaction by interaction ID", + Long: "Cancels an interaction by id. This only applies to background interactions that are still running.", + Example: " gemini-api agent cancel --id ", + Args: cobra.NoArgs, + RunE: runCancelCmd, + Annotations: map[string]string{ + "speakeasy_operation": "cancelInteractionById", + }, + } + flagutil.RegisterFlags(cmd, cancelCmdMeta) + if err := flagutil.ValidateMeta[operations.CancelInteractionByIDRequest](cancelCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for cancel: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runCancelCmd executes the cancel command. +func runCancelCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.CancelInteractionByIDRequest](cmd, cancelCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.Cancel(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/agent/create.go b/internal/cli/agent/create.go new file mode 100644 index 0000000..493eb89 --- /dev/null +++ b/internal/cli/agent/create.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var createCmdMeta = []flagutil.FlagMeta{ + {FlagName: "agent-config", Shorthand: "a", FieldPath: "Body.AgentConfig", Kind: flagutil.FlagKindUnion, Union: &flagutil.UnionMeta{Discriminated: false, Optional: true, TypeDescription: "JSON value (one of: { \"max_total_tokens\": string, \"model\": string })"}}, + {FlagName: "base-agent", FieldPath: "Body.BaseAgent", Kind: flagutil.FlagKindString, Required: true, Description: "The base agent to extend. [required]"}, + {FlagName: "base-environment", FieldPath: "Body.BaseEnvironment", Kind: flagutil.FlagKindUnion, Union: &flagutil.UnionMeta{Discriminated: false, Optional: true, TypeDescription: "JSON value (one of: { \"env\": object, \"environment_id\": string, \"network\": object | string | string, \"sources\": object[] } | string)"}}, + {FlagName: "description", FieldPath: "Body.Description", Kind: flagutil.FlagKindString, Optional: true, Description: "Agent description for developers to quickly read and understand."}, + {FlagName: "id", Shorthand: "i", FieldPath: "Body.ID", Kind: flagutil.FlagKindString, Required: true, Description: "The unique identifier for the agent. [required]"}, + {FlagName: "system-instruction", Shorthand: "s", FieldPath: "Body.SystemInstruction", Kind: flagutil.FlagKindString, Optional: true, Description: "System instruction for the agent."}, + {FlagName: "tools", Shorthand: "t", FieldPath: "Body.Tools", Kind: flagutil.FlagKindJSON, Optional: true, Annotations: `json:"tools,omitempty"`, Description: "The tools available to the agent."}, +} + +// initCreateCmd initializes the create command. +func initCreateCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "create", + Short: "Create a managed agent definition", + Long: "Creates a new Agent (Typed version for SDK).", + Example: " gemini-api agent create --base-agent --id ", + Args: cobra.NoArgs, + RunE: runCreateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateAgent", + }, + } + flagutil.RegisterFlags(cmd, createCmdMeta) + if err := flagutil.ValidateMeta[operations.CreateAgentRequest](createCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for create: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, createCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for create: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runCreateCmd executes the create command. +func runCreateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateAgent") + } + req, err := flagutil.BuildRequest[operations.CreateAgentRequest](cmd, createCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.Create(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/agent/delete.go b/internal/cli/agent/delete.go new file mode 100644 index 0000000..8ffe2a0 --- /dev/null +++ b/internal/cli/agent/delete.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var deleteCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, +} + +// initDeleteCmd initializes the delete command. +func initDeleteCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete", + Short: "Delete a managed agent definition by ID", + Long: "Deletes an Agent.", + Example: " gemini-api agent delete --id ", + Args: cobra.NoArgs, + RunE: runDeleteCmd, + Annotations: map[string]string{ + "speakeasy_operation": "DeleteAgent", + }, + } + flagutil.RegisterFlags(cmd, deleteCmdMeta) + if err := flagutil.ValidateMeta[operations.DeleteAgentRequest](deleteCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for delete: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runDeleteCmd executes the delete command. +func runDeleteCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.DeleteAgentRequest](cmd, deleteCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.Delete(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/agent/deleteinteraction.go b/internal/cli/agent/deleteinteraction.go new file mode 100644 index 0000000..de5625c --- /dev/null +++ b/internal/cli/agent/deleteinteraction.go @@ -0,0 +1,90 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var deleteInteractionCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "The unique identifier of the interaction to delete. [required]"}, +} + +// initDeleteInteractionCmd initializes the delete-interaction command. +func initDeleteInteractionCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete-interaction", + Short: "Delete an interaction by interaction ID", + Long: "Deletes the interaction by id.", + Example: "", + Args: cobra.NoArgs, + RunE: runDeleteInteractionCmd, + Aliases: []string{"di"}, + Annotations: map[string]string{ + "speakeasy_operation": "deleteInteraction", + }, + } + flagutil.RegisterFlags(cmd, deleteInteractionCmdMeta) + if err := flagutil.ValidateMeta[operations.DeleteInteractionRequest](deleteInteractionCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for delete-interaction: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runDeleteInteractionCmd executes the delete-interaction command. +func runDeleteInteractionCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.DeleteInteractionRequest](cmd, deleteInteractionCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.DeleteInteraction(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/agent/get.go b/internal/cli/agent/get.go new file mode 100644 index 0000000..61177be --- /dev/null +++ b/internal/cli/agent/get.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var getCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, +} + +// initGetCmd initializes the get command. +func initGetCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Get a managed agent definition by ID", + Long: "Gets a specific Agent.", + Example: " gemini-api agent get --id ", + Args: cobra.NoArgs, + RunE: runGetCmd, + Annotations: map[string]string{ + "speakeasy_operation": "GetAgent", + }, + } + flagutil.RegisterFlags(cmd, getCmdMeta) + if err := flagutil.ValidateMeta[operations.GetAgentRequest](getCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for get: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runGetCmd executes the get command. +func runGetCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetAgentRequest](cmd, getCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.Get(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/agent/intent_agent-run.go b/internal/cli/agent/intent_agent-run.go new file mode 100644 index 0000000..8b43dc9 --- /dev/null +++ b/internal/cli/agent/intent_agent-run.go @@ -0,0 +1,320 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitIntentAgentRun(parent *cobra.Command) error { + cmd := &cobra.Command{ + Use: "run [input]", + Short: "Run an interaction with a Gemini model or a managed agent", + Long: "Run one interaction with a Gemini model (--model, the default) or a\nmanaged agent (--agent); the two flag sets are mutually exclusive.\nStreams text as it arrives; --stream=false returns one complete\ninteraction. --body takes the exact request JSON (\"model\"/\"agent\" picks the variant).\n\nArguments:\n Prompt or task to send\n\nRequest variants: Agent (--agent), Model (--model; default).\nVariant-specific flags cannot be combined.", + Example: " gemini-api agent run \"Analyze market trends for Q3\" --agent deep-research-preview-04-2026\n gemini-api agent run \"Write a detailed research report on solar batteries\" --background\n gemini-api agent run \"Explain the difference between concurrency and parallelism\" --model gemini-3.6-flash", + Args: cobra.ArbitraryArgs, + RunE: runIntentAgentRunCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateInteraction", + flagutil.AnnotationWholeBodyFlag: "body", + "speakeasy_strict_body_keys": "true", + "speakeasy_help_defaults": "model gemini-3.6-flash · stream true", + "speakeasy_help_escalate": "exact request JSON via --body @request.json (schema with --schema)", + "speakeasy_stream_select": "/data/delta/text", + }, + } + intentMeta := flagutil.NonBodyMeta(runCmdMeta, "Body") + flagutil.RegisterFlags(cmd, intentMeta) + flagutil.SetMetaPromptOptional(cmd, intentMeta, false) + cmd.Flags().String("body", "", "Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + cmd.Flags().StringP("agent", "", "", "Managed agent to run (see \"gemini-api agent list\") (e.g. deep-research-pro-preview-12-2025, deep-research-preview-04-2026, deep-research-max-preview-04-2026, antigravity-preview-05-2026)") + _ = flagutil.AnnotatePromptFlag(cmd, "agent", flagutil.PromptFlagSpec{ + Required: false, + Kind: "string", + Order: 0, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body"}, + }) + _ = cmd.Flags().SetAnnotation("agent", "speakeasy:group", []string{"Agent variant"}) + _ = cmd.Flags().SetAnnotation("agent", "speakeasy:group-order", []string{"0"}) + cmd.Flags().BoolP("background", "", false, "Return immediately with an interaction ID; poll with \"gemini-api agent status\"") + _ = flagutil.AnnotatePromptFlag(cmd, "background", flagutil.PromptFlagSpec{ + Required: false, + Kind: "bool", + Order: 1, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body"}, + }) + cmd.Flags().StringP("model", "m", "", "Model to run (see \"gemini-api models\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)") + _ = flagutil.AnnotatePromptFlag(cmd, "model", flagutil.PromptFlagSpec{ + Required: false, + Kind: "string", + Order: 2, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body"}, + }) + _ = cmd.Flags().SetAnnotation("model", "speakeasy:group", []string{"Model variant"}) + _ = cmd.Flags().SetAnnotation("model", "speakeasy:group-order", []string{"1"}) + cmd.Flags().BoolP("stream", "", false, "Stream the reply as it is generated; use --stream=false for one complete interaction (default: true)") + _ = flagutil.AnnotatePromptFlag(cmd, "stream", flagutil.PromptFlagSpec{ + Required: false, + Kind: "bool", + Order: 3, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body"}, + }) + if err := interactive.Declare(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{ + { + Name: "input", Summary: "Prompt or task to send", + Required: true, Variadic: true, + BodyKey: "input", + SatisfiedBy: []string{"body"}, + }, + }}); err != nil { + return fmt.Errorf("declare interactive arguments for intent agent-run: %w", err) + } + for _, sibling := range parent.Commands() { + if sibling.Name() == cmd.Name() || sibling.HasAlias(cmd.Name()) { + return fmt.Errorf("intent command %q collides with the name or alias of an existing %q command; rename the declared command", "agent-run", sibling.Name()) + } + } + parent.AddCommand(cmd) + return nil +} + +var intentAgentRunDispatch = flagutil.DispatchTable{ + Command: "agent run", + BodyFlag: "body", + Escape: "gemini-api agent run", + Routes: []flagutil.DispatchRoute{ + { + ID: "agent", Label: "Agent", + SelectorFlag: "agent", Default: false, + PresetJSON: "", + PresetMerge: flagutil.PresetMerge{ + Command: "agent run", Variant: "CreateAgentInteractionParams", + Preset: "", + Foreign: []string{"model"}, + Escape: "gemini-api agent run", + }, + }, + { + ID: "model", Label: "Model", + SelectorFlag: "model", Default: true, + PresetJSON: "", + PresetMerge: flagutil.PresetMerge{ + Command: "agent run", Variant: "CreateModelInteractionParams", + Preset: "", + Foreign: []string{"agent"}, + Escape: "gemini-api agent run", + }, + }, + }, + Inputs: []flagutil.DispatchInput{ + { + Name: "input", BodyKey: "input", + Kind: flagutil.FlagKindString, Positional: true, + RouteIDs: []string{"agent", "model"}, + RequiredRouteIDs: []string{"agent", "model"}, + }, + { + Name: "agent", BodyKey: "agent", + Kind: flagutil.FlagKindString, Positional: false, + RouteIDs: []string{"agent"}, + RequiredRouteIDs: []string{"agent"}, + }, + { + Name: "background", BodyKey: "background", + Kind: flagutil.FlagKindBool, Positional: false, + RouteIDs: []string{"agent", "model"}, + RequiredRouteIDs: []string{}, + }, + { + Name: "model", BodyKey: "model", + Kind: flagutil.FlagKindString, Positional: false, + RouteIDs: []string{"model"}, + RequiredRouteIDs: []string{}, + }, + { + Name: "stream", BodyKey: "stream", + Kind: flagutil.FlagKindBool, Positional: false, + RouteIDs: []string{"agent", "model"}, + RequiredRouteIDs: []string{}, + }, + }, + Keys: []flagutil.DispatchKey{ + { + BodyKey: "agent", + RouteIDs: []string{"agent"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "agent_config", + RouteIDs: []string{"agent"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "background", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "created", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "environment", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "environment_id", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "generation_config", + RouteIDs: []string{"model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "id", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "input", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "labels", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "model", + RouteIDs: []string{"model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "previous_interaction_id", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "response_format", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "response_mime_type", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "response_modalities", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "safety_settings", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "service_tier", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "status", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "store", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "stream", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "system_instruction", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "tools", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "updated", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + { + BodyKey: "webhook_config", + RouteIDs: []string{"agent", "model"}, + UnroutedVariants: []string{}, + }, + }, +} + +func runIntentAgentRunCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateInteraction") + } + if len(args) == 0 && !flagutil.FlagChanged(cmd, "agent") && !flagutil.FlagChanged(cmd, "background") && !flagutil.FlagChanged(cmd, "model") && !flagutil.FlagChanged(cmd, "stream") { + bodySupplied, err := flagutil.PrimeDispatchBody(cmd, "body") + if err != nil { + return err + } + if !bodySupplied { + return output.UsageHelpError(cmd, fmt.Errorf("%s", "missing required argument (or pass a full request with --body)")) + } + } + if hint := flagutil.SpacedBoolValueHint(cmd, args); hint != "" && !client.IsJSONDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), hint) + } + if _, err := flagutil.Select(cmd, args, intentAgentRunDispatch); err != nil { + return err + } + return runRunCmd(cmd, nil) +} diff --git a/internal/cli/agent/intent_generate.go b/internal/cli/agent/intent_generate.go new file mode 100644 index 0000000..2de1cae --- /dev/null +++ b/internal/cli/agent/intent_generate.go @@ -0,0 +1,203 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitIntentGenerate(parent *cobra.Command) error { + cmd := &cobra.Command{ + Use: "generate [prompt]", + Short: "Text & multimodal generation (gemini-3.6-flash)", + Long: "Send a prompt to a Gemini model and print the reply as it is\ngenerated; text deltas are streamed directly to stdout. Uses the\nschema's default model (gemini-3.6-flash) unless --model or a full\n--body names one. --stream=false returns one complete interaction;\n--output-format json shows the raw NDJSON event stream. Thinking\nmodels such as gemini-3.6-flash may emit their deltas in a burst\nafter thinking; gemini-2.5-flash streams visibly. Full request\ncontrol: \"gemini-api agent run\".\n\nArguments:\n Prompt to send to the model", + Example: " gemini-api generate \"Write a haiku about APIs\" --model gemini-2.5-pro\n gemini-api generate \"Explain concurrency in one sentence\"", + Args: cobra.ArbitraryArgs, + RunE: runIntentGenerateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateInteraction", + flagutil.AnnotationWholeBodyFlag: "body", + "speakeasy_strict_body_keys": "true", + "speakeasy_help_defaults": "model gemini-3.6-flash · streams the reply (--stream=false for one result)", + "speakeasy_help_learn": "https://ai.google.dev/gemini-api/docs/text-generation", + "speakeasy_help_escalate": "full request control via gemini-api agent run", + "speakeasy_stream_select": "/data/delta/text", + }, + } + flagutil.RegisterFlags(cmd, runCmdMeta) + flagutil.SetMetaPromptOptional(cmd, runCmdMeta, false) + flagutil.ClearBodyRequirements(cmd, runCmdMeta, "Body") + cmd.Flags().String("body", "", "Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + if err := flagutil.AnnotateBodyFields(cmd, runCmdMeta, "Body", "body", "body-param"); err != nil { + return fmt.Errorf("annotate body fields for intent generate: %w", err) + } + _ = flagutil.MarkBodyFlag(cmd, "body-param") + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + cmd.Flags().StringP("model", "m", "", "Model to use (see \"gemini-api models\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)") + _ = flagutil.AnnotatePromptFlag(cmd, "model", flagutil.PromptFlagSpec{ + Required: false, + Kind: "string", + Order: 0, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body", "body-param"}, + }) + cmd.Flags().BoolP("stream", "", false, "Stream the reply as it is generated; use --stream=false for a single complete result (default: true)") + _ = flagutil.AnnotatePromptFlag(cmd, "stream", flagutil.PromptFlagSpec{ + Required: false, + Kind: "bool", + Order: 1, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body", "body-param"}, + }) + if err := interactive.Declare(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{ + { + Name: "prompt", Summary: "Prompt to send to the model", + Required: true, Variadic: true, + BodyKey: "input", + SatisfiedBy: []string{"body", "body-param"}, + }, + }}); err != nil { + return fmt.Errorf("declare interactive arguments for intent generate: %w", err) + } + for _, sibling := range parent.Commands() { + if sibling.Name() == cmd.Name() || sibling.HasAlias(cmd.Name()) { + return fmt.Errorf("intent command %q collides with the name or alias of an existing %q command; rename the declared command", "generate", sibling.Name()) + } + } + parent.AddCommand(cmd) + return nil +} + +var intentGeneratePreset = flagutil.PresetMerge{ + Command: "generate", + Variant: "CreateModelInteractionParams", + Preset: "{\"stream\":true}", + Foreign: []string{"agent"}, + Escape: "gemini-api agent run", +} + +func runIntentGenerateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateInteraction") + } + bodySurfaces := []string{"body", "body-param"} + for _, surface := range bodySurfaces { + if flagutil.FlagChanged(cmd, surface) { + if err := flagutil.ResolveBodyFlag(cmd, surface); err != nil { + return err + } + } + } + suppliedBodyFlag := "" + if flagutil.FlagChanged(cmd, "body-param") { + suppliedBodyFlag = "body-param" + } + if flagutil.FlagChanged(cmd, "body") { + suppliedBodyFlag = "body" + } + bodySupplied := suppliedBodyFlag != "" + if !bodySupplied { + attached, err := flagutil.AttachStdinBody(cmd, "body") + if err != nil { + return err + } + bodySupplied = attached + } + if hint := flagutil.SpacedBoolValueHint(cmd, args); hint != "" && !client.IsJSONDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), hint) + } + if bodySupplied { + if len(args) > 0 { + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "input", "the argument", strings.Join(args, " ")); err != nil { + return flagutil.WithCLIValidation(err) + } + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "model", "--model", v); err != nil { + return flagutil.WithCLIValidation(err) + } + } + if flagutil.FlagChanged(cmd, "stream") { + v, _ := cmd.Flags().GetBool("stream") + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "stream", "--stream", v); err != nil { + return flagutil.WithCLIValidation(err) + } + } + } + if len(args) == 0 && !bodySupplied && !flagutil.FlagChanged(cmd, "body") && !flagutil.FlagChanged(cmd, "body-param") && !flagutil.FlagChanged(cmd, "model") && !flagutil.FlagChanged(cmd, "stream") { + return output.UsageHelpError(cmd, fmt.Errorf("%s", "missing required argument (or pass a full request with --body)")) + } + if !bodySupplied { + body := map[string]any{} + if err := json.Unmarshal([]byte(intentGeneratePreset.Preset), &body); err != nil { + return err + } + if len(args) == 0 { + return flagutil.WithCLIValidation(fmt.Errorf("missing required argument (or pass a full request with --body)")) + } + if len(args) > 0 { + body["input"] = strings.Join(args, " ") + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + body["model"] = v + } + if flagutil.FlagChanged(cmd, "stream") { + v, _ := cmd.Flags().GetBool("stream") + body["stream"] = v + } + encoded, err := json.Marshal(body) + if err != nil { + return err + } + if err := cmd.Flags().Set("body", string(encoded)); err != nil { + return err + } + } else { + for _, surface := range bodySurfaces { + if !flagutil.FlagChanged(cmd, surface) { + continue + } + raw, _ := flagutil.GetStringFlag(cmd, surface) + merged, err := flagutil.MergePresetBody(raw, intentGeneratePreset) + if err != nil { + return err + } + if err := cmd.Flags().Set(surface, merged); err != nil { + return err + } + } + } + return runRunCmd(cmd, nil) +} diff --git a/internal/cli/agent/intent_image.go b/internal/cli/agent/intent_image.go new file mode 100644 index 0000000..1f64674 --- /dev/null +++ b/internal/cli/agent/intent_image.go @@ -0,0 +1,194 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitIntentImage(parent *cobra.Command) error { + cmd := &cobra.Command{ + Use: "image [prompt]", + Short: "Generate or edit images (gemini-3.1-flash-image)", + Long: "Generate an image from a text prompt via the Interactions API\n(image response modality) and write it to a file; stdout\ncarries the file path. Use --out to choose the file (or a\ndirectory), --raw-response to see the API response instead.\n\nArguments:\n Image prompt", + Example: " gemini-api image \"a lighthouse at sunset\"\n gemini-api image \"product shot, white bg\" --out shots/hero.png", + Args: cobra.ArbitraryArgs, + RunE: runIntentImageCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateInteraction", + flagutil.AnnotationWholeBodyFlag: "body", + "speakeasy_strict_body_keys": "true", + "speakeasy_help_defaults": "model gemini-3.1-flash-image · output ./gemini-image-{timestamp}-{rand}.{ext}", + "speakeasy_help_learn": "https://ai.google.dev/gemini-api/docs/image-generation", + "speakeasy_help_escalate": "full request control via gemini-api agent run", + "speakeasy_artifact": "{\"pointer\":[{\"field\":\"steps\"},{\"wild\":true},{\"field\":\"content\"},{\"wild\":true}],\"kind\":\"image\",\"defaultPath\":\"gemini-image-{timestamp}-{rand}.{ext}\"}", + }, + } + flagutil.RegisterFlags(cmd, runCmdMeta) + flagutil.SetMetaPromptOptional(cmd, runCmdMeta, false) + flagutil.ClearBodyRequirements(cmd, runCmdMeta, "Body") + cmd.Flags().String("body", "", "Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + if err := flagutil.AnnotateBodyFields(cmd, runCmdMeta, "Body", "body", "body-param"); err != nil { + return fmt.Errorf("annotate body fields for intent image: %w", err) + } + _ = flagutil.MarkBodyFlag(cmd, "body-param") + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + cmd.Flags().StringP("model", "m", "", "Override the image model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)") + _ = flagutil.AnnotatePromptFlag(cmd, "model", flagutil.PromptFlagSpec{ + Required: false, + Kind: "string", + Order: 0, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body", "body-param"}, + }) + if err := interactive.Declare(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{ + { + Name: "prompt", Summary: "Image prompt", + Required: true, Variadic: true, + BodyKey: "input", + SatisfiedBy: []string{"body", "body-param"}, + }, + }}); err != nil { + return fmt.Errorf("declare interactive arguments for intent image: %w", err) + } + cmd.Flags().String("out", "", "Write the image to this file (or into this directory). Default: ./gemini-image-{timestamp}-{rand}.{ext}") + _ = flagutil.AnnotatePromptFlag(cmd, "out", flagutil.PromptFlagSpec{ + PromptDirect: true, + Label: "Output file", + Kind: "string", + }) + cmd.Flags().Bool("raw-response", false, "Print the raw API response instead of writing the image to a file") + for _, sibling := range parent.Commands() { + if sibling.Name() == cmd.Name() || sibling.HasAlias(cmd.Name()) { + return fmt.Errorf("intent command %q collides with the name or alias of an existing %q command; rename the declared command", "image", sibling.Name()) + } + } + parent.AddCommand(cmd) + return nil +} + +var intentImagePreset = flagutil.PresetMerge{ + Command: "image", + Variant: "CreateModelInteractionParams", + Preset: "{\"model\":\"gemini-3.1-flash-image\",\"response_format\":{\"type\":\"image\"},\"stream\":false}", + Foreign: []string{"agent"}, + Escape: "gemini-api agent run", +} + +func runIntentImageCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateInteraction") + } + if flagutil.FlagChanged(cmd, "raw-response") && flagutil.FlagChanged(cmd, "out") { + return flagutil.WithCLIValidation(fmt.Errorf("--raw-response prints the raw API response and cannot be combined with --out")) + } + bodySurfaces := []string{"body", "body-param"} + for _, surface := range bodySurfaces { + if flagutil.FlagChanged(cmd, surface) { + if err := flagutil.ResolveBodyFlag(cmd, surface); err != nil { + return err + } + } + } + suppliedBodyFlag := "" + if flagutil.FlagChanged(cmd, "body-param") { + suppliedBodyFlag = "body-param" + } + if flagutil.FlagChanged(cmd, "body") { + suppliedBodyFlag = "body" + } + bodySupplied := suppliedBodyFlag != "" + if !bodySupplied { + attached, err := flagutil.AttachStdinBody(cmd, "body") + if err != nil { + return err + } + bodySupplied = attached + } + if hint := flagutil.SpacedBoolValueHint(cmd, args); hint != "" && !client.IsJSONDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), hint) + } + if bodySupplied { + if len(args) > 0 { + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "input", "the argument", strings.Join(args, " ")); err != nil { + return flagutil.WithCLIValidation(err) + } + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "model", "--model", v); err != nil { + return flagutil.WithCLIValidation(err) + } + } + } + if len(args) == 0 && !bodySupplied && !flagutil.FlagChanged(cmd, "body") && !flagutil.FlagChanged(cmd, "body-param") && !flagutil.FlagChanged(cmd, "model") { + return output.UsageHelpError(cmd, fmt.Errorf("%s", "missing required argument (or pass a full request with --body)")) + } + if !bodySupplied { + body := map[string]any{} + if err := json.Unmarshal([]byte(intentImagePreset.Preset), &body); err != nil { + return err + } + if len(args) == 0 { + return flagutil.WithCLIValidation(fmt.Errorf("missing required argument (or pass a full request with --body)")) + } + if len(args) > 0 { + body["input"] = strings.Join(args, " ") + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + body["model"] = v + } + encoded, err := json.Marshal(body) + if err != nil { + return err + } + if err := cmd.Flags().Set("body", string(encoded)); err != nil { + return err + } + } else { + for _, surface := range bodySurfaces { + if !flagutil.FlagChanged(cmd, surface) { + continue + } + raw, _ := flagutil.GetStringFlag(cmd, surface) + merged, err := flagutil.MergePresetBody(raw, intentImagePreset) + if err != nil { + return err + } + if err := cmd.Flags().Set(surface, merged); err != nil { + return err + } + } + } + return runRunCmd(cmd, nil) +} diff --git a/internal/cli/agent/intent_music.go b/internal/cli/agent/intent_music.go new file mode 100644 index 0000000..3e13043 --- /dev/null +++ b/internal/cli/agent/intent_music.go @@ -0,0 +1,194 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitIntentMusic(parent *cobra.Command) error { + cmd := &cobra.Command{ + Use: "music [prompt]", + Short: "Music generation (lyria-3-pro-preview)", + Long: "Generate music via the Interactions API (audio response\nmodality).\n\nArguments:\n Music prompt", + Example: " gemini-api music \"upbeat synthwave with a driving bassline\"", + Args: cobra.ArbitraryArgs, + RunE: runIntentMusicCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateInteraction", + flagutil.AnnotationWholeBodyFlag: "body", + "speakeasy_strict_body_keys": "true", + "speakeasy_help_defaults": "model lyria-3-pro-preview · output ./gemini-music-{timestamp}-{rand}.{ext}", + "speakeasy_help_learn": "https://ai.google.dev/gemini-api/docs/music-generation", + "speakeasy_help_escalate": "full request control via gemini-api agent run", + "speakeasy_artifact": "{\"pointer\":[{\"field\":\"steps\"},{\"wild\":true},{\"field\":\"content\"},{\"wild\":true}],\"kind\":\"audio\",\"defaultPath\":\"gemini-music-{timestamp}-{rand}.{ext}\"}", + }, + } + flagutil.RegisterFlags(cmd, runCmdMeta) + flagutil.SetMetaPromptOptional(cmd, runCmdMeta, false) + flagutil.ClearBodyRequirements(cmd, runCmdMeta, "Body") + cmd.Flags().String("body", "", "Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + if err := flagutil.AnnotateBodyFields(cmd, runCmdMeta, "Body", "body", "body-param"); err != nil { + return fmt.Errorf("annotate body fields for intent music: %w", err) + } + _ = flagutil.MarkBodyFlag(cmd, "body-param") + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + cmd.Flags().StringP("model", "m", "", "Override the music model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)") + _ = flagutil.AnnotatePromptFlag(cmd, "model", flagutil.PromptFlagSpec{ + Required: false, + Kind: "string", + Order: 0, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body", "body-param"}, + }) + if err := interactive.Declare(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{ + { + Name: "prompt", Summary: "Music prompt", + Required: true, Variadic: true, + BodyKey: "input", + SatisfiedBy: []string{"body", "body-param"}, + }, + }}); err != nil { + return fmt.Errorf("declare interactive arguments for intent music: %w", err) + } + cmd.Flags().String("out", "", "Write the audio to this file (or into this directory). Default: ./gemini-music-{timestamp}-{rand}.{ext}") + _ = flagutil.AnnotatePromptFlag(cmd, "out", flagutil.PromptFlagSpec{ + PromptDirect: true, + Label: "Output file", + Kind: "string", + }) + cmd.Flags().Bool("raw-response", false, "Print the raw API response instead of writing the audio to a file") + for _, sibling := range parent.Commands() { + if sibling.Name() == cmd.Name() || sibling.HasAlias(cmd.Name()) { + return fmt.Errorf("intent command %q collides with the name or alias of an existing %q command; rename the declared command", "music", sibling.Name()) + } + } + parent.AddCommand(cmd) + return nil +} + +var intentMusicPreset = flagutil.PresetMerge{ + Command: "music", + Variant: "CreateModelInteractionParams", + Preset: "{\"model\":\"lyria-3-pro-preview\",\"response_format\":{\"type\":\"audio\"},\"stream\":false}", + Foreign: []string{"agent"}, + Escape: "gemini-api agent run", +} + +func runIntentMusicCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateInteraction") + } + if flagutil.FlagChanged(cmd, "raw-response") && flagutil.FlagChanged(cmd, "out") { + return flagutil.WithCLIValidation(fmt.Errorf("--raw-response prints the raw API response and cannot be combined with --out")) + } + bodySurfaces := []string{"body", "body-param"} + for _, surface := range bodySurfaces { + if flagutil.FlagChanged(cmd, surface) { + if err := flagutil.ResolveBodyFlag(cmd, surface); err != nil { + return err + } + } + } + suppliedBodyFlag := "" + if flagutil.FlagChanged(cmd, "body-param") { + suppliedBodyFlag = "body-param" + } + if flagutil.FlagChanged(cmd, "body") { + suppliedBodyFlag = "body" + } + bodySupplied := suppliedBodyFlag != "" + if !bodySupplied { + attached, err := flagutil.AttachStdinBody(cmd, "body") + if err != nil { + return err + } + bodySupplied = attached + } + if hint := flagutil.SpacedBoolValueHint(cmd, args); hint != "" && !client.IsJSONDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), hint) + } + if bodySupplied { + if len(args) > 0 { + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "input", "the argument", strings.Join(args, " ")); err != nil { + return flagutil.WithCLIValidation(err) + } + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "model", "--model", v); err != nil { + return flagutil.WithCLIValidation(err) + } + } + } + if len(args) == 0 && !bodySupplied && !flagutil.FlagChanged(cmd, "body") && !flagutil.FlagChanged(cmd, "body-param") && !flagutil.FlagChanged(cmd, "model") { + return output.UsageHelpError(cmd, fmt.Errorf("%s", "missing required argument (or pass a full request with --body)")) + } + if !bodySupplied { + body := map[string]any{} + if err := json.Unmarshal([]byte(intentMusicPreset.Preset), &body); err != nil { + return err + } + if len(args) == 0 { + return flagutil.WithCLIValidation(fmt.Errorf("missing required argument (or pass a full request with --body)")) + } + if len(args) > 0 { + body["input"] = strings.Join(args, " ") + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + body["model"] = v + } + encoded, err := json.Marshal(body) + if err != nil { + return err + } + if err := cmd.Flags().Set("body", string(encoded)); err != nil { + return err + } + } else { + for _, surface := range bodySurfaces { + if !flagutil.FlagChanged(cmd, surface) { + continue + } + raw, _ := flagutil.GetStringFlag(cmd, surface) + merged, err := flagutil.MergePresetBody(raw, intentMusicPreset) + if err != nil { + return err + } + if err := cmd.Flags().Set(surface, merged); err != nil { + return err + } + } + } + return runRunCmd(cmd, nil) +} diff --git a/internal/cli/agent/intent_video.go b/internal/cli/agent/intent_video.go new file mode 100644 index 0000000..810591d --- /dev/null +++ b/internal/cli/agent/intent_video.go @@ -0,0 +1,240 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitIntentVideo(parent *cobra.Command) error { + cmd := &cobra.Command{ + Use: "video [prompt]", + Short: "Generate & edit video conversationally (gemini-omni-flash-preview)", + Long: "Generate video via the Interactions API (video response\nmodality). The interaction runs in the background: the CLI\npolls \"agent status\" with backoff until it completes, writes\nthe video to a file, and prints the file path; --async returns\nthe interaction ID immediately instead (resume with\n\"gemini-api agent status --id \"). A requires_action result\nis printed as-is. Use --out to choose the file (or a\ndirectory), --raw-response to see the API response instead.\n\nArguments:\n Video prompt", + Example: " gemini-api video \"a timelapse of a city at night\" --async\n gemini-api video \"a timelapse of a city at night\"", + Args: cobra.ArbitraryArgs, + RunE: runIntentVideoCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateInteraction", + flagutil.AnnotationWholeBodyFlag: "body", + "speakeasy_strict_body_keys": "true", + "speakeasy_help_defaults": "model gemini-omni-flash-preview · polls until done, writes ./gemini-video-.mp4 (--async returns the interaction ID)", + "speakeasy_help_learn": "https://ai.google.dev/gemini-api/docs/video", + "speakeasy_help_escalate": "full request control via gemini-api agent run", + "speakeasy_artifact": "{\"pointer\":[{\"field\":\"steps\"},{\"wild\":true},{\"field\":\"content\"},{\"wild\":true}],\"kind\":\"video\",\"defaultPath\":\"gemini-video-{timestamp}-{rand}.{ext}\"}", + "speakeasy_async": "{\"idPointer\":\"/id\",\"statePointer\":\"/status\",\"states\":{\"budget_exceeded\":\"failure\",\"cancelled\":\"failure\",\"completed\":\"success\",\"failed\":\"failure\",\"in_progress\":\"pending\",\"incomplete\":\"failure\",\"queued\":\"pending\",\"requires_action\":\"handoff\"},\"interval\":\"5s\",\"backoff\":1.5,\"maxInterval\":\"30s\",\"timeout\":\"15m\",\"command\":\"video\",\"resume\":\"gemini-api agent status --id\",\"parameterIn\":\"path\",\"parameterName\":\"id\",\"params\":[{\"in\":\"query\",\"name\":\"stream\",\"value\":false}]}", + }, + } + flagutil.RegisterFlags(cmd, runCmdMeta) + flagutil.SetMetaPromptOptional(cmd, runCmdMeta, false) + flagutil.ClearBodyRequirements(cmd, runCmdMeta, "Body") + cmd.Flags().String("body", "", "Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + if err := flagutil.AnnotateBodyFields(cmd, runCmdMeta, "Body", "body", "body-param"); err != nil { + return fmt.Errorf("annotate body fields for intent video: %w", err) + } + _ = flagutil.MarkBodyFlag(cmd, "body-param") + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + cmd.Flags().StringP("model", "m", "", "Override the video model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)") + _ = flagutil.AnnotatePromptFlag(cmd, "model", flagutil.PromptFlagSpec{ + Required: false, + Kind: "string", + Order: 0, + // A supplied whole body carries this flag's bound key (and the + // backing operation flag supplies it directly): no prompt then. + BodySources: []string{"body", "body-param"}, + }) + if err := interactive.Declare(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{ + { + Name: "prompt", Summary: "Video prompt", + Required: true, Variadic: true, + BodyKey: "input", + SatisfiedBy: []string{"body", "body-param"}, + }, + }}); err != nil { + return fmt.Errorf("declare interactive arguments for intent video: %w", err) + } + cmd.Flags().String("out", "", "Write the video to this file (or into this directory). Default: ./gemini-video-{timestamp}-{rand}.{ext}") + _ = flagutil.AnnotatePromptFlag(cmd, "out", flagutil.PromptFlagSpec{ + PromptDirect: true, + Label: "Output file", + Kind: "string", + }) + cmd.Flags().Bool("raw-response", false, "Print the raw API response instead of writing the video to a file") + cmd.Flags().Bool("async", false, "Return the operation handle without waiting for a terminal response") + cmd.Flags().String("poll-interval", "", "Override the initial polling interval (positive Go duration, for example 500ms or 2s)") + cmd.Flags().String("poll-timeout", "", "Override the overall polling deadline (positive Go duration, at least the effective poll interval)") + _ = flagutil.AnnotatePromptFlag(cmd, "async", flagutil.PromptFlagSpec{Kind: "bool"}) + _ = flagutil.AnnotatePromptFlag(cmd, "poll-interval", flagutil.PromptFlagSpec{Kind: "string"}) + _ = flagutil.AnnotatePromptFlag(cmd, "poll-timeout", flagutil.PromptFlagSpec{Kind: "string"}) + for _, sibling := range parent.Commands() { + if sibling.Name() == cmd.Name() || sibling.HasAlias(cmd.Name()) { + return fmt.Errorf("intent command %q collides with the name or alias of an existing %q command; rename the declared command", "video", sibling.Name()) + } + } + parent.AddCommand(cmd) + return nil +} + +var intentVideoPreset = flagutil.PresetMerge{ + Command: "video", + Variant: "CreateModelInteractionParams", + Preset: "{\"background\":true,\"model\":\"gemini-omni-flash-preview\",\"response_format\":{\"type\":\"video\"},\"stream\":false}", + Foreign: []string{"agent"}, + Escape: "gemini-api agent run", +} + +func runIntentVideoCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateInteraction") + } + if flagutil.FlagChanged(cmd, "raw-response") && flagutil.FlagChanged(cmd, "out") { + return flagutil.WithCLIValidation(fmt.Errorf("--raw-response prints the raw API response and cannot be combined with --out")) + } + bodySurfaces := []string{"body", "body-param"} + for _, surface := range bodySurfaces { + if flagutil.FlagChanged(cmd, surface) { + if err := flagutil.ResolveBodyFlag(cmd, surface); err != nil { + return err + } + } + } + suppliedBodyFlag := "" + if flagutil.FlagChanged(cmd, "body-param") { + suppliedBodyFlag = "body-param" + } + if flagutil.FlagChanged(cmd, "body") { + suppliedBodyFlag = "body" + } + bodySupplied := suppliedBodyFlag != "" + if !bodySupplied { + attached, err := flagutil.AttachStdinBody(cmd, "body") + if err != nil { + return err + } + bodySupplied = attached + } + if hint := flagutil.SpacedBoolValueHint(cmd, args); hint != "" && !client.IsJSONDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), hint) + } + if bodySupplied { + if len(args) > 0 { + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "input", "the argument", strings.Join(args, " ")); err != nil { + return flagutil.WithCLIValidation(err) + } + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + if err := flagutil.MergeInputIntoBody(cmd, suppliedBodyFlag, "model", "--model", v); err != nil { + return flagutil.WithCLIValidation(err) + } + } + } + if len(args) == 0 && !bodySupplied && !flagutil.FlagChanged(cmd, "body") && !flagutil.FlagChanged(cmd, "body-param") && !flagutil.FlagChanged(cmd, "model") { + return output.UsageHelpError(cmd, fmt.Errorf("%s", "missing required argument (or pass a full request with --body)")) + } + if !bodySupplied { + body := map[string]any{} + if err := json.Unmarshal([]byte(intentVideoPreset.Preset), &body); err != nil { + return err + } + if len(args) == 0 { + return flagutil.WithCLIValidation(fmt.Errorf("missing required argument (or pass a full request with --body)")) + } + if len(args) > 0 { + body["input"] = strings.Join(args, " ") + } + if flagutil.FlagChanged(cmd, "model") { + v, _ := flagutil.GetStringFlag(cmd, "model") + body["model"] = v + } + encoded, err := json.Marshal(body) + if err != nil { + return err + } + if err := cmd.Flags().Set("body", string(encoded)); err != nil { + return err + } + } else { + for _, surface := range bodySurfaces { + if !flagutil.FlagChanged(cmd, surface) { + continue + } + raw, _ := flagutil.GetStringFlag(cmd, surface) + merged, err := flagutil.MergePresetBody(raw, intentVideoPreset) + if err != nil { + return err + } + if err := cmd.Flags().Set(surface, merged); err != nil { + return err + } + } + } + if err := output.ValidateAsyncFlags(cmd); err != nil { + return err + } + res, err := executeRunCmd(cmd, nil, true) + if err != nil { + return err + } + if client.IsDryRun(cmd) { + return nil + } + return output.AsyncResult(cmd, res, newPollIntentVideo) +} +func newPollIntentVideo(cmd *cobra.Command, id string, params []output.AsyncParameter) (output.AsyncPollFunc, error) { + request := &operations.GetInteractionByIDRequest{} + if err := flagutil.SetRequestParameter(request, "status", "path", "id", id); err != nil { + return nil, err + } + for _, param := range params { + if err := flagutil.SetRequestParameter(request, "status", param.In, param.Name, param.Value); err != nil { + return nil, err + } + } + s, err := client.NewClient(cmd) + if err != nil { + return nil, err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return nil, err + } + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + return func(ctx context.Context) (interface{}, error) { + res, err := s.Agent.Status(ctx, *request, sdkOpts...) + if err != nil { + return nil, err + } + return res, nil + }, nil +} diff --git a/internal/cli/agent/list.go b/internal/cli/agent/list.go new file mode 100644 index 0000000..984fad4 --- /dev/null +++ b/internal/cli/agent/list.go @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listCmdMeta = []flagutil.FlagMeta{ + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "integer value"}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "string value"}, + {FlagName: "parent", FieldPath: "Parent", Kind: flagutil.FlagKindString, Optional: true, Description: "string value"}, +} + +// initListCmd initializes the list command. +func initListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "List managed agent definitions", + Long: "Lists all Agents.", + Example: " gemini-api agent list", + Args: cobra.NoArgs, + RunE: runListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ListAgents", + }, + } + flagutil.RegisterFlags(cmd, listCmdMeta) + if err := flagutil.ValidateMeta[operations.ListAgentsRequest](listCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list: %w", err) + } + cmd.Flags().BoolP("all", "a", false, "Automatically paginate and fetch all results (streams NDJSON for JSON output)") + cmd.Flags().Int("max-pages", 0, "Maximum number of pages to fetch when using --all (0 = no limit)") + parent.AddCommand(cmd) + return nil +} + +// runListCmd executes the list command. +func runListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + allPages, _ := flagutil.GetBoolFlag(cmd, "all") + maxPages, _ := flagutil.GetIntFlag(cmd, "max-pages") + if maxPages < 0 { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages must be zero or greater")) + } + if flagutil.FlagChanged(cmd, "max-pages") && !allPages { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages requires --all")) + } + req, err := flagutil.BuildRequest[operations.ListAgentsRequest](cmd, listCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if allPages && !client.IsDryRun(cmd) { + res, err := s.Agent.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.PaginatedResult(cmd, res, "AgentListResponse", "", maxPages, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + morePages := output.HasMorePages(res, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + + if err := output.Result(cmd, res); err != nil { + return err + } + if morePages && !client.IsDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), "Hint: more pages available. Use --all to fetch all results, or --page-token for manual pagination.") + } + return nil +} diff --git a/internal/cli/agent/root.go b/internal/cli/agent/root.go new file mode 100644 index 0000000..1628204 --- /dev/null +++ b/internal/cli/agent/root.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitAgentRoot(parent *cobra.Command) error { + var AgentCmd = &cobra.Command{ + Use: "agent", + Short: "Run interactions with Gemini models or managed agents, and manage agent definitions", + Long: "Run interactions with Gemini models or managed agents, and manage agent definitions.\n\nStart here:\n gemini-api agent run --help\n\nOther common flows:\n gemini-api agent create --help Define a managed agent\n gemini-api agent status --help Inspect a background interaction\n\nNote: agent IDs and interaction IDs are distinct resources. \"agent status\"\ntakes an interaction ID; to inspect an agent definition use \"agent get\".", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initListCmd(AgentCmd); err != nil { + return err + } + + if err := initCreateCmd(AgentCmd); err != nil { + return err + } + + if err := initDeleteCmd(AgentCmd); err != nil { + return err + } + + if err := initGetCmd(AgentCmd); err != nil { + return err + } + + if err := initDeleteInteractionCmd(AgentCmd); err != nil { + return err + } + + if err := initStatusCmd(AgentCmd); err != nil { + return err + } + + if err := initCancelCmd(AgentCmd); err != nil { + return err + } + + parent.AddCommand(AgentCmd) + return nil +} diff --git a/internal/cli/agent/run.go b/internal/cli/agent/run.go new file mode 100644 index 0000000..610c2ca --- /dev/null +++ b/internal/cli/agent/run.go @@ -0,0 +1,133 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var runCmdMeta = []flagutil.FlagMeta{ + {FlagName: "body-param", Shorthand: "b", FieldPath: "Body", Kind: flagutil.FlagKindUnion, Union: &flagutil.UnionMeta{Discriminated: false, VariantKeys: []string{"agent", "model"}, DefaultJSON: "{\"model\":\"gemini-3.6-flash\"}", TypeDescription: "JSON value (one of: { \"agent\": string, \"input\": object | object[] | string, \"stream\": boolean (default: true), ... } | { \"input\": object | object[] | string, \"model\": string (default: gemini-3.6-flash), \"stream\": boolean (default: true), ... }; default when neither is named: {\"model\":\"gemini-3.6-flash\"})"}}, +} + +// initRunCmd initializes the run command. +func initRunCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "run", + Short: "Run an interaction with a Gemini model or a managed agent", + Long: "Run one interaction with either a Gemini model or an existing managed agent.\nProvide input and choose exactly one selector: \"model\" or \"agent\".\n\nFor a first model run, use gemini-3.6-flash (recommended starting model).\nOther model IDs: https://ai.google.dev/gemini-api/docs/models\n\nPass the full JSON with --body or stdin. Set \"background\": true to\nreturn immediately with an interaction ID, then poll:\n gemini-api agent status --id \n\nBy default, streamed responses write the string selected by $.data.delta.text raw as it arrives. Use --stream=false to request one complete JSON response; use -o json to keep each full streamed event.", + Example: " gemini-api agent run --body '{\"environment\":\"\",\"generation_config\":{\"tool_choice\":{\"allowed_tools\":{\"mode\":\"any\",\"tools\":[\"my_tool\"]\x7d\x7d},\"input\":\"\",\"model\":\"gemini-3.6-flash\",\"response_format\":{\"0\":{\"type\":\"text\",\"mime_type\":\"application/json\"\x7d\x7d,\"stream\":true}'\n gemini-api agent run --body '{\"background\":true,\"input\":\"Write a detailed research report on solar batteries\",\"model\":\"gemini-3.6-flash\",\"stream\":true}'\n gemini-api agent run --body '{\"input\":\"Explain the difference between concurrency and parallelism\",\"model\":\"gemini-3.6-flash\",\"stream\":true}'", + Args: cobra.NoArgs, + RunE: runRunCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateInteraction", + "speakeasy_stream_select": "/data/delta/text", + }, + } + flagutil.RegisterFlags(cmd, runCmdMeta) + if err := flagutil.ValidateMeta[operations.CreateInteractionRequest](runCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for run: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, runCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for run: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + cmd.Flags().BoolP("stream", "", true, "Stream the reply as it is generated; use --stream=false for one complete interaction") + if err := cmd.Flags().SetAnnotation("stream", flagutil.AnnotationOpDeclaredInput, []string{"stream"}); err != nil { + return err + } + _ = flagutil.AnnotatePromptFlag(cmd, "stream", flagutil.PromptFlagSpec{ + PromptOptional: true, + Kind: "bool", + DefaultResolves: true, + }) + parent.AddCommand(cmd) + return nil +} + +// runRunCmd executes the run command. +func runRunCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateInteraction") + } + if err := flagutil.MergeOperationDeclaredInputs(cmd, []string{ + "body", + "body-param", + }, "body-param", true); err != nil { + return err + } + res, err := executeRunCmd(cmd, args, false) + if err != nil { + return err + } + if res == nil { + return nil + } + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} +func executeRunCmd(cmd *cobra.Command, args []string, asyncIntent bool) (*operations.CreateInteractionResponse, error) { + req, err := flagutil.BuildRequest[operations.CreateInteractionRequest](cmd, runCmdMeta, "Body", "body") + if err != nil { + return nil, flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return nil, err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return nil, err + } + if client.IsDryRun(cmd) || asyncIntent { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if !client.IsDryRun(cmd) && !asyncIntent { + res, err := s.Agent.Run(cmd.Context(), *req, sdkOpts...) + if err != nil { + return nil, output.Error(cmd, err) + } + if err := output.StreamResult(cmd, res, "InteractionSSEStreamEvent"); err != nil { + return nil, err + } + return nil, nil + } + if asyncIntent || output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.Run(cmd.Context(), *req, sdkOpts...) + if err != nil { + return nil, output.Error(cmd, err) + } + return res, nil +} diff --git a/internal/cli/agent/status.go b/internal/cli/agent/status.go new file mode 100644 index 0000000..dda3bb9 --- /dev/null +++ b/internal/cli/agent/status.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agent + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var statusCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "The unique identifier of the interaction to retrieve. [required]"}, + {FlagName: "include-input", FieldPath: "IncludeInput", Kind: flagutil.FlagKindBool, Optional: true, HasDefault: true, Description: "If set to true, includes the input in the response."}, + {FlagName: "last-event-id", Shorthand: "l", FieldPath: "LastEventID", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. If set, resumes the interaction stream from the next chunk after the event marked by the event id. Can only be used if `stream` is true."}, + {FlagName: "stream", Shorthand: "s", FieldPath: "Stream", Kind: flagutil.FlagKindBool, Optional: true, HasDefault: true, DefaultBool: true, Description: "Stream the interaction's events (replayed from the start for a finished interaction) instead of returning the status object. Defaults to true; use --stream=false for the status object."}, +} + +// initStatusCmd initializes the status command. +func initStatusCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "status", + Short: "Get status and output of an interaction by interaction ID", + Long: "Get the status and output of an interaction by interaction ID. Use this to poll a background run started with \"agent run\".\n\nStreamed responses write the string selected by $.data.delta.text raw as it arrives. Pass --stream to request a streamed response; use -o json to keep each full streamed event.", + Example: " gemini-api agent status --id ", + Args: cobra.NoArgs, + RunE: runStatusCmd, + Annotations: map[string]string{ + "speakeasy_operation": "getInteractionById", + "speakeasy_stream_select": "/data/delta/text", + }, + } + flagutil.RegisterFlags(cmd, statusCmdMeta) + if err := flagutil.ValidateMeta[operations.GetInteractionByIDRequest](statusCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for status: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runStatusCmd executes the status command. +func runStatusCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetInteractionByIDRequest](cmd, statusCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + // Streaming response — iterate events and output incrementally. + // Skip streaming iteration in dry-run mode (synthetic response has no stream). + if !client.IsDryRun(cmd) { + res, err := s.Agent.Status(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.StreamResult(cmd, res, "InteractionSSEStreamEvent") + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Agent.Status(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/auth.go b/internal/cli/auth.go new file mode 100644 index 0000000..946064b --- /dev/null +++ b/internal/cli/auth.go @@ -0,0 +1,316 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "fmt" + "os" + + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/spf13/cobra" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" +) + +// initAuthCmd registers the auth command group with login, whoami, and logout subcommands. +func initAuthCmd(parent *cobra.Command) error { + var authCmd *cobra.Command + for _, existing := range parent.Commands() { + if existing.Name() == "auth" || existing.HasAlias("auth") { + authCmd = existing + break + } + } + if authCmd == nil { + authCmd = &cobra.Command{ + Use: "auth", + Short: "Manage authentication credentials", + Long: `Manage authentication credentials for gemini-api. + +Subcommands: + login - Interactively configure credentials + whoami - Display current authentication status + logout - Clear all stored credentials`, + } + parent.AddCommand(authCmd) + } + + addAuthSubcommand := func(sub *cobra.Command) { + for _, existing := range authCmd.Commands() { + if existing.Name() == sub.Name() || existing.HasAlias(sub.Name()) { + return + } + } + authCmd.AddCommand(sub) + } + + addAuthSubcommand(&cobra.Command{ + Use: "login", + Short: "Interactively configure authentication credentials", + Long: `Interactively configure authentication credentials for gemini-api. +Secret credentials are stored in the OS keychain when available, +with a config file fallback. + +All fields are optional — press Enter to skip any field you don't need. +Use the configure command for both authentication and global parameters.`, + Args: cobra.NoArgs, + RunE: runAuthLoginCmd, + }) + + addAuthSubcommand(&cobra.Command{ + Use: "whoami", + Short: "Display current authentication and global parameter configuration", + Long: `Display the currently configured settings and their sources. + +Sources are shown as: + [flag] - Set via command line flag + [env] - Set via environment variable (GEMINI_*) + [keyring] - Set via OS keychain (stored by login/configure command) + [config] - Set via config file (~/.config/gemini-api/config.yaml) + [unset] - Not configured + +Credential values are masked for security.`, + Args: cobra.NoArgs, + RunE: runWhoamiCmd, + }) + + addAuthSubcommand(&cobra.Command{ + Use: "logout", + Short: "Clear all stored authentication credentials", + Long: `Clear all stored authentication credentials from both the OS keychain and config file. + +This removes all credentials previously set via auth login or configure.`, + Args: cobra.NoArgs, + RunE: runAuthLogoutCmd, + }) + + return nil +} + +// runAuthLoginCmd executes the auth login command using huh forms. +func runAuthLoginCmd(cmd *cobra.Command, args []string) error { + if dryRunLocalNoop(cmd, "auth login changes local credentials only (no API request); nothing was changed.") { + return nil + } + cfg := config.GetConfig() + if cfg == nil { + cfg = &config.Config{} + } + + keychainStored := false + formMode := interactive.Resolve(cmd).FormMode() + + if formMode == interactive.FormOff { + // Non-interactive: store any explicitly-set flags without prompting + changed := false + if f := cmd.Flags().Lookup("api-key"); f != nil && f.Changed { + v, _ := cmd.Flags().GetString("api-key") + if config.StoreSecret("api-key", v, &cfg.Security.ApiKey) == nil { + keychainStored = true + } + changed = true + } + if f := cmd.Flags().Lookup("access-token"); f != nil && f.Changed { + v, _ := cmd.Flags().GetString("access-token") + if config.StoreSecret("access-token", v, &cfg.Security.AccessToken) == nil { + keychainStored = true + } + changed = true + } + + if !changed { + return flagutil.WithCLIValidation(fmt.Errorf("no flags provided; use flags to store credentials in %s, or pass --interactive to open the form", config.GetConfigPath())) + } + } else { + + accessible := formMode == interactive.FormAccessible + + var selectedScheme string + schemeSelect := huh.NewSelect[string](). + Title("Authentication Method"). + Description("Choose which credentials to configure"). + Options( + huh.NewOption("Gemini API key sent as x-goog-api-key.", "api-key"), + huh.NewOption("OAuth access token sent as a bearer Authorization header.", "access-token"), + ). + Value(&selectedScheme) + + if err := huh.NewForm(huh.NewGroup(schemeSelect)).WithAccessible(accessible).WithTheme(authFormTheme()).WithWidth(authFormWidth()).WithShowHelp(false).Run(); err != nil { + return fmt.Errorf("auth login: %w", err) + } + + switch selectedScheme { + case "api-key": + var authApiKey string + + fields := []huh.Field{ + huh.NewInput(). + Title("Gemini API key sent as x-goog-api-key."). + Description("--api-key"). + EchoMode(huh.EchoModePassword). + Placeholder(maskSecret(config.GetStoredSecret("api-key", cfg.Security.ApiKey))). + Value(&authApiKey), + } + + form := huh.NewForm(huh.NewGroup(fields...)). + WithAccessible(accessible). + WithTheme(authFormTheme()). + WithWidth(authFormWidth()). + WithShowHelp(false) + + if err := form.Run(); err != nil { + return fmt.Errorf("auth login: %w", err) + } + + if authApiKey != "" { + if config.StoreSecret("api-key", authApiKey, &cfg.Security.ApiKey) == nil { + keychainStored = true + } + } + + case "access-token": + var authAccessToken string + + fields := []huh.Field{ + huh.NewInput(). + Title("OAuth access token sent as a bearer Authorization header."). + Description("--access-token"). + EchoMode(huh.EchoModePassword). + Placeholder(maskSecret(config.GetStoredSecret("access-token", cfg.Security.AccessToken))). + Value(&authAccessToken), + } + + form := huh.NewForm(huh.NewGroup(fields...)). + WithAccessible(accessible). + WithTheme(authFormTheme()). + WithWidth(authFormWidth()). + WithShowHelp(false) + + if err := form.Run(); err != nil { + return fmt.Errorf("auth login: %w", err) + } + + if authAccessToken != "" { + if config.StoreSecret("access-token", authAccessToken, &cfg.Security.AccessToken) == nil { + keychainStored = true + } + } + + } + + } + + if err := config.SaveConfig(cfg); err != nil { + return fmt.Errorf("failed to save configuration: %w", err) + } + + out := cmd.OutOrStdout() + if keychainStored { + fmt.Fprintln(out, "Secret credentials stored in OS keychain") + } + fmt.Fprintf(out, "Configuration saved to %s\n", config.GetConfigPath()) + return nil +} + +// runAuthLogoutCmd clears all stored authentication credentials. +func runAuthLogoutCmd(cmd *cobra.Command, args []string) error { + if dryRunLocalNoop(cmd, "auth logout removes local credentials only (no API request); nothing was changed.") { + return nil + } + cfg := config.GetConfig() + if cfg == nil { + cfg = &config.Config{} + } + + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("api-key") + } + cfg.Security.ApiKey = "" + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("access-token") + } + cfg.Security.AccessToken = "" + + if err := config.SaveConfig(cfg); err != nil { + return fmt.Errorf("failed to save configuration: %w", err) + } + + out := cmd.OutOrStdout() + fmt.Fprintln(out, "All authentication credentials have been cleared.") + fmt.Fprintf(out, "Configuration saved to %s\n", config.GetConfigPath()) + return nil +} + +// authFormTheme builds the form theme for auth login. +func authFormTheme() *huh.Theme { + t := *huh.ThemeBase() + + accent := lipgloss.Color("#38BDF8") + dimmed := lipgloss.Color("#64748B") + subtle := lipgloss.Color("#475569") + errColor := lipgloss.Color("#F87171") + greenColor := lipgloss.Color("#4ADE80") + + t.Focused.Base = t.Focused.Base. + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(accent). + PaddingLeft(1) + t.Focused.Title = t.Focused.Title.Foreground(accent).Bold(true) + t.Focused.Description = t.Focused.Description.Foreground(dimmed).Italic(true) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errColor) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errColor) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accent).SetString("> ") + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(accent).Bold(true) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(greenColor).SetString("✓ ").Bold(true) + t.Focused.UnselectedPrefix = lipgloss.NewStyle().SetString(" ") + t.Focused.FocusedButton = t.Focused.FocusedButton.Background(accent).Foreground(lipgloss.Color("#FFFFFF")) + t.Focused.BlurredButton = t.Focused.BlurredButton.Background(subtle) + t.Focused.Next = t.Focused.FocusedButton + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(accent) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(subtle).Italic(true) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accent) + + t.Blurred.Base = t.Blurred.Base. + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(subtle). + PaddingLeft(1) + t.Blurred.Title = t.Blurred.Title.Foreground(dimmed) + t.Blurred.Description = t.Blurred.Description.Foreground(subtle).Italic(true) + t.Blurred.TextInput.Text = t.Blurred.TextInput.Text.Foreground(dimmed) + t.Blurred.TextInput.Placeholder = t.Blurred.TextInput.Placeholder.Foreground(subtle).Italic(true) + t.Blurred.SelectedOption = t.Blurred.SelectedOption.Foreground(dimmed) + t.Blurred.SelectSelector = t.Blurred.SelectSelector.Foreground(dimmed) + t.Blurred.SelectedPrefix = lipgloss.NewStyle().Foreground(dimmed).SetString("✓ ") + t.Blurred.UnselectedPrefix = lipgloss.NewStyle().SetString(" ") + + return &t +} + +// authFormWidth returns the terminal width for sizing huh forms. +func authFormWidth() int { + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || width <= 0 { + width = 80 + } + return width +} diff --git a/internal/cli/catalog.go b/internal/cli/catalog.go new file mode 100644 index 0000000..7cd580e --- /dev/null +++ b/internal/cli/catalog.go @@ -0,0 +1,117 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/spf13/cobra" +) + +func initCatalogCmds(parent *cobra.Command) error { + if err := addOrMergeCatalogCmd(parent, newModelsCatalogCmd()); err != nil { + return fmt.Errorf("register models catalog: %w", err) + } + return nil +} + +func addOrMergeCatalogCmd(parent *cobra.Command, catalogCmd *cobra.Command) error { + for _, existing := range parent.Commands() { + exactName := existing.Name() == catalogCmd.Name() + if !exactName && !existing.HasAlias(catalogCmd.Name()) { + continue + } + if exactName && existing.Annotations["speakeasy_cli_group"] == "true" && existing.Annotations["speakeasy_cli_promoted"] != "true" { + existing.RunE = catalogCmd.RunE + existing.Short = catalogCmd.Short + if catalogCmd.Long != "" { + existing.Long = catalogCmd.Long + } + return nil + } + return fmt.Errorf("x-speakeasy-cli-catalog command %q collides with the existing %q command; rename the catalog command", catalogCmd.Name(), existing.CommandPath()) + } + parent.AddCommand(catalogCmd) + return nil +} + +func newModelsCatalogCmd() *cobra.Command { + return &cobra.Command{ + Use: "models", + Short: "List available models and the default", + Long: "Curated model list generated from the API schema. The default model is used by \"agent run\" whenever the request body names neither \"model\" nor \"agent\".", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + values := []map[string]interface{}{ + {"value": "gemini-2.5-flash", "description": "Our first hybrid reasoning model which supports a 1M token context window and has thinking budgets.", "default": false}, + {"value": "gemini-2.5-pro", "description": "Our state-of-the-art multipurpose model, which excels at coding and complex reasoning tasks.", "default": false}, + {"value": "gemma-4-26b-a4b-it", "description": "Gemma 4 26B A4B IT", "default": false}, + {"value": "gemma-4-31b-it", "description": "Gemma 4 31B IT", "default": false}, + {"value": "gemini-flash-latest", "description": "Latest release of Gemini Flash", "default": false}, + {"value": "gemini-flash-lite-latest", "description": "Latest release of Gemini Flash-Lite", "default": false}, + {"value": "gemini-pro-latest", "description": "Latest release of Gemini Pro", "default": false}, + {"value": "gemini-2.5-flash-lite", "description": "Our smallest and most cost effective model, built for at scale usage.", "default": false}, + {"value": "gemini-2.5-flash-image", "description": "Our native image generation model, optimized for speed, flexibility, and contextual understanding. Text input and output is priced the same as 2.5 Flash.", "default": false}, + {"value": "gemini-3-flash-preview", "description": "Our most intelligent model built for speed, combining frontier intelligence with superior search and grounding.", "default": false}, + {"value": "gemini-3.1-pro-preview", "description": "Our latest SOTA reasoning model with unprecedented depth and nuance, and powerful multimodal understanding and coding capabilities.", "default": false}, + {"value": "gemini-3.1-pro-preview-customtools", "description": "Gemini 3.1 Pro Preview optimized for custom tool usage", "default": false}, + {"value": "gemini-3.1-flash-lite", "description": "Our most cost-efficient model, optimized for high-volume agentic tasks, translation, and simple data processing.", "default": false}, + {"value": "gemini-3-pro-image", "description": "Gemini 3 Pro Image", "default": false}, + {"value": "nano-banana-pro-preview", "description": "Gemini 3 Pro Image Preview", "default": false}, + {"value": "gemini-3.1-flash-image", "description": "Gemini 3.1 Flash Image.", "default": false}, + {"value": "gemini-3.5-flash", "description": "Gemini 3.5 Flash - Our earlier Flash model, built for speed and foundational performance across routine, high-throughput workloads.", "default": false}, + {"value": "gemini-3.6-flash", "description": "Gemini 3.6 Flash - Our previous generation Flash model, balancing speed and multimodal capabilities across general agentic and everyday tasks.", "default": true}, + {"value": "gemini-3.7-flash", "description": "Gemini 3.7 Flash - Our high-speed, efficient Flash model built for everyday coding, agentic tool use, and reliable multi-step execution.", "default": false}, + {"value": "gemini-3.8-flash", "description": "Gemini 3.8 Flash - Our most intelligent Flash model, engineered for long-horizon software engineering, autonomous agents, and complex enterprise workflows.", "default": false}, + {"value": "lyria-3-clip-preview", "description": "Our low-latency, music generation model optimized for high-fidelity audio clips and precise rhythmic control.", "default": false}, + {"value": "lyria-3-pro-preview", "description": "Our advanced, full-song generative model with deep compositional understanding, optimized for precise structural control and complex transitions across diverse musical styles.", "default": false}, + {"value": "gemini-robotics-er-1.6-preview", "description": "Gemini Robotics-ER 1.6 Preview", "default": false}, + {"value": "gemini-robotics-er-2-preview", "description": "Gemini Robotics Embodied Reasoning 2 Preview", "default": false}, + } + if output.IsMachineMode(cmd) { + return output.LocalResult(cmd, values) + } + out := cmd.OutOrStdout() + fmt.Fprintf(out, "%-42s %s\n", "gemini-2.5-flash", "Our first hybrid reasoning model which supports a 1M token context window and has thinking budgets.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-2.5-pro", "Our state-of-the-art multipurpose model, which excels at coding and complex reasoning tasks.") + fmt.Fprintf(out, "%-42s %s\n", "gemma-4-26b-a4b-it", "Gemma 4 26B A4B IT") + fmt.Fprintf(out, "%-42s %s\n", "gemma-4-31b-it", "Gemma 4 31B IT") + fmt.Fprintf(out, "%-42s %s\n", "gemini-flash-latest", "Latest release of Gemini Flash") + fmt.Fprintf(out, "%-42s %s\n", "gemini-flash-lite-latest", "Latest release of Gemini Flash-Lite") + fmt.Fprintf(out, "%-42s %s\n", "gemini-pro-latest", "Latest release of Gemini Pro") + fmt.Fprintf(out, "%-42s %s\n", "gemini-2.5-flash-lite", "Our smallest and most cost effective model, built for at scale usage.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-2.5-flash-image", "Our native image generation model, optimized for speed, flexibility, and contextual understanding. Text input and output is priced the same as 2.5 Flash.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3-flash-preview", "Our most intelligent model built for speed, combining frontier intelligence with superior search and grounding.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.1-pro-preview", "Our latest SOTA reasoning model with unprecedented depth and nuance, and powerful multimodal understanding and coding capabilities.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.1-pro-preview-customtools", "Gemini 3.1 Pro Preview optimized for custom tool usage") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.1-flash-lite", "Our most cost-efficient model, optimized for high-volume agentic tasks, translation, and simple data processing.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3-pro-image", "Gemini 3 Pro Image") + fmt.Fprintf(out, "%-42s %s\n", "nano-banana-pro-preview", "Gemini 3 Pro Image Preview") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.1-flash-image", "Gemini 3.1 Flash Image.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.5-flash", "Gemini 3.5 Flash - Our earlier Flash model, built for speed and foundational performance across routine, high-throughput workloads.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.6-flash (default)", "Gemini 3.6 Flash - Our previous generation Flash model, balancing speed and multimodal capabilities across general agentic and everyday tasks.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.7-flash", "Gemini 3.7 Flash - Our high-speed, efficient Flash model built for everyday coding, agentic tool use, and reliable multi-step execution.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-3.8-flash", "Gemini 3.8 Flash - Our most intelligent Flash model, engineered for long-horizon software engineering, autonomous agents, and complex enterprise workflows.") + fmt.Fprintf(out, "%-42s %s\n", "lyria-3-clip-preview", "Our low-latency, music generation model optimized for high-fidelity audio clips and precise rhythmic control.") + fmt.Fprintf(out, "%-42s %s\n", "lyria-3-pro-preview", "Our advanced, full-song generative model with deep compositional understanding, optimized for precise structural control and complex transitions across diverse musical styles.") + fmt.Fprintf(out, "%-42s %s\n", "gemini-robotics-er-1.6-preview", "Gemini Robotics-ER 1.6 Preview") + fmt.Fprintf(out, "%-42s %s\n", "gemini-robotics-er-2-preview", "Gemini Robotics Embodied Reasoning 2 Preview") + return nil + }, + } +} diff --git a/internal/cli/configure.go b/internal/cli/configure.go new file mode 100644 index 0000000..22e2753 --- /dev/null +++ b/internal/cli/configure.go @@ -0,0 +1,300 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "cmp" + "encoding/json" + "fmt" + "os" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" +) + +// initConfigureCmd initializes the configure command. +func initConfigureCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "configure", + Short: "Configure authentication, global parameters, and preferences", + Long: `Interactively configure authentication credentials, global parameters, and preferences for the CLI. +Settings are stored in ~/.config/gemini-api/config.yaml. +Secret credentials are stored in the OS keychain when available. + +You can also set values via environment variables with the GEMINI_ prefix +(e.g., GEMINI_API_KEY) or pass them as flags to individual commands. + +Priority: CLI flags > environment variables > OS keychain > config file`, + Args: cobra.NoArgs, + RunE: runConfigureCmd, + } + parent.AddCommand(cmd) + return nil +} + +// runConfigureCmd executes the configure command. +func runConfigureCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if dryRunLocalNoop(cmd, "configure changes local settings only (no API request); nothing was changed.") { + return nil + } + cfg := config.GetConfig() + if cfg == nil { + cfg = &config.Config{} + } + + keychainStored := false + + formMode := interactive.Resolve(cmd).FormMode() + if formMode == interactive.FormOff { + changed := false + if f := cmd.Flags().Lookup("api-key"); f != nil && f.Changed { + v, _ := cmd.Flags().GetString("api-key") + if config.StoreSecret("api-key", v, &cfg.Security.ApiKey) == nil { + keychainStored = true + } + changed = true + } + if f := cmd.Flags().Lookup("access-token"); f != nil && f.Changed { + v, _ := cmd.Flags().GetString("access-token") + if config.StoreSecret("access-token", v, &cfg.Security.AccessToken) == nil { + keychainStored = true + } + changed = true + } + if f := cmd.Flags().Lookup("api-version"); f != nil && f.Changed { + cfg.Globals.ApiVersion = f.Value.String() + changed = true + } + if f := cmd.Flags().Lookup("api-revision"); f != nil && f.Changed { + cfg.Globals.ApiRevision = f.Value.String() + changed = true + } + if f := cmd.Flags().Lookup("user-project"); f != nil && f.Changed { + cfg.Globals.UserProject = f.Value.String() + changed = true + } + + if !changed { + return flagutil.WithCLIValidation(fmt.Errorf("no flags provided; use flags to store values in %s, or pass --interactive to open the form", config.GetConfigPath())) + } + } else { + var authApiKey string + var authAccessToken string + var cfgGlobalApiVersion string + var cfgGlobalApiRevision string + var cfgGlobalUserProject string + accessible := formMode == interactive.FormAccessible + + var groups []*huh.Group + securityFields := []huh.Field{ + huh.NewInput(). + Title("Gemini API key sent as x-goog-api-key."). + Description("--api-key"). + EchoMode(huh.EchoModePassword). + Placeholder(maskSecret(config.GetStoredSecret("api-key", cfg.Security.ApiKey))). + Value(&authApiKey), + huh.NewInput(). + Title("OAuth access token sent as a bearer Authorization header."). + Description("--access-token"). + EchoMode(huh.EchoModePassword). + Placeholder(maskSecret(config.GetStoredSecret("access-token", cfg.Security.AccessToken))). + Value(&authAccessToken), + } + groups = append(groups, huh.NewGroup(securityFields...).Title("Authentication")) + globalFields := []huh.Field{ + huh.NewInput(). + Title("Which version of the API to use"). + Description("--api-version"). + Placeholder(cmp.Or(cfg.Globals.ApiVersion, "v1beta")). + Value(&cfgGlobalApiVersion), + huh.NewInput(). + Title("Interactions API revision to request"). + Description("--api-revision"). + Placeholder(cfg.Globals.ApiRevision). + Value(&cfgGlobalApiRevision), + huh.NewInput(). + Title("Quota project header to send with Google GenAI API requests"). + Description("--user-project"). + Placeholder(cfg.Globals.UserProject). + Value(&cfgGlobalUserProject), + } + groups = append(groups, huh.NewGroup(globalFields...).Title("Global Parameters")) + + // Preference fields use huh.Select which loops forever on EOF in + // accessible mode (non-TTY). Only show them when truly interactive. + var cfgOutputFormat string + if !accessible { + preferenceFields := []huh.Field{ + huh.NewSelect[string](). + Title("Default output format"). + Description("Choose the default response rendering format for this CLI"). + Options( + huh.NewOption("Keep current", ""), + huh.NewOption("Clear (use built-in default: pretty)", "__CLEAR__"), + huh.NewOption("pretty", "pretty"), + huh.NewOption("json", "json"), + huh.NewOption("yaml", "yaml"), + huh.NewOption("table", "table"), + huh.NewOption("toon", "toon"), + ). + Value(&cfgOutputFormat), + } + groups = append(groups, huh.NewGroup(preferenceFields...).Title("Preferences")) + } + + form := huh.NewForm(groups...). + WithAccessible(accessible). + WithTheme(configureFormTheme()). + WithWidth(configureFormWidth()). + WithShowHelp(false) + + if err := form.Run(); err != nil { + return fmt.Errorf("configure: %w", err) + } + if authApiKey != "" { + if config.StoreSecret("api-key", authApiKey, &cfg.Security.ApiKey) == nil { + keychainStored = true + } + } + + if authAccessToken != "" { + if config.StoreSecret("access-token", authAccessToken, &cfg.Security.AccessToken) == nil { + keychainStored = true + } + } + if cfgGlobalApiVersion != "" { + cfg.Globals.ApiVersion = cfgGlobalApiVersion + } + + if cfgGlobalApiRevision != "" { + cfg.Globals.ApiRevision = cfgGlobalApiRevision + } + + if cfgGlobalUserProject != "" { + cfg.Globals.UserProject = cfgGlobalUserProject + } + if !accessible { + switch cfgOutputFormat { + case "__CLEAR__": + cfg.OutputFormat = "" + case "pretty", "json", "yaml", "table", "toon": + cfg.OutputFormat = cfgOutputFormat + } + } + } + + if err := config.SaveConfig(cfg); err != nil { + return fmt.Errorf("failed to save configuration: %w", err) + } + + out := cmd.OutOrStdout() + if keychainStored { + fmt.Fprintln(out, "Secret credentials stored in OS keychain") + } + fmt.Fprintf(out, "Configuration saved to %s\n", config.GetConfigPath()) + return nil +} + +// dryRunLocalNoop implements the append-safe dry-run contract for local +// mutation commands: no prompts, keychain access, or filesystem writes. The +// machine preview protocol still receives an explicit local no-op record — +// silence would be indistinguishable from a failed preview. +func dryRunLocalNoop(cmd *cobra.Command, message string) bool { + if !client.IsDryRun(cmd) { + return false + } + if client.IsJSONDryRun(cmd) { + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetEscapeHTML(false) + _ = enc.Encode(struct { + DryRun bool `json:"dry_run"` + Local bool `json:"local"` + Command string `json:"command"` + Message string `json:"message"` + }{DryRun: true, Local: true, Command: cmd.CommandPath(), Message: message}) + } else { + fmt.Fprintln(cmd.ErrOrStderr(), "[DRY-RUN] "+message) + } + return true +} + +// configureFormTheme builds the form theme for the configure command. +func configureFormTheme() *huh.Theme { + t := *huh.ThemeBase() + + accent := lipgloss.Color("#38BDF8") + dimmed := lipgloss.Color("#64748B") + subtle := lipgloss.Color("#475569") + errColor := lipgloss.Color("#F87171") + greenColor := lipgloss.Color("#4ADE80") + + t.Focused.Base = t.Focused.Base. + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(accent). + PaddingLeft(1) + t.Focused.Title = t.Focused.Title.Foreground(accent).Bold(true) + t.Focused.Description = t.Focused.Description.Foreground(dimmed).Italic(true) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errColor) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errColor) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accent).SetString("> ") + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(accent).Bold(true) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(greenColor).SetString("✓ ").Bold(true) + t.Focused.UnselectedPrefix = lipgloss.NewStyle().SetString(" ") + t.Focused.FocusedButton = t.Focused.FocusedButton.Background(accent).Foreground(lipgloss.Color("#FFFFFF")) + t.Focused.BlurredButton = t.Focused.BlurredButton.Background(subtle) + t.Focused.Next = t.Focused.FocusedButton + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(accent) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(subtle).Italic(true) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accent) + + t.Blurred.Base = t.Blurred.Base. + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(subtle). + PaddingLeft(1) + t.Blurred.Title = t.Blurred.Title.Foreground(dimmed) + t.Blurred.Description = t.Blurred.Description.Foreground(subtle).Italic(true) + t.Blurred.TextInput.Text = t.Blurred.TextInput.Text.Foreground(dimmed) + t.Blurred.TextInput.Placeholder = t.Blurred.TextInput.Placeholder.Foreground(subtle).Italic(true) + t.Blurred.SelectedOption = t.Blurred.SelectedOption.Foreground(dimmed) + t.Blurred.SelectSelector = t.Blurred.SelectSelector.Foreground(dimmed) + t.Blurred.SelectedPrefix = lipgloss.NewStyle().Foreground(dimmed).SetString("✓ ") + t.Blurred.UnselectedPrefix = lipgloss.NewStyle().SetString(" ") + + return &t +} + +// configureFormWidth returns the terminal width for sizing huh forms. +func configureFormWidth() int { + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || width <= 0 { + width = 80 + } + return width +} diff --git a/internal/cli/credentials/create.go b/internal/cli/credentials/create.go new file mode 100644 index 0000000..ae5e824 --- /dev/null +++ b/internal/cli/credentials/create.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var createCmdMeta = []flagutil.FlagMeta{ + {FlagName: "body-param", Shorthand: "b", FieldPath: "Body", Kind: flagutil.FlagKindUnion, Union: &flagutil.UnionMeta{Discriminated: true, DiscriminatorKey: "Type", TypeDescription: "JSON value (variants: environment_variable: { \"id\": string, \"injection_location\": string | string[], \"value\": string, ... }, bearer_token: { \"id\": string, \"token\": string, ... }, oauth2: { \"client_id\": string, \"client_secret\": string, \"id\": string, \"refresh_token\": string, ... })", Variants: []flagutil.UnionVariantMeta{ + {DiscriminatorValue: "environment_variable", FlagName: "body-param.environment-variable", FieldName: "EnvironmentVariableConfig", CanExpand: false, Description: "EnvironmentVariableConfig variant as JSON"}, + {DiscriminatorValue: "bearer_token", FlagName: "body-param.bearer-token", FieldName: "HTTPBearerConfig", CanExpand: true, Description: "HttpBearerConfig variant as JSON", Fields: []flagutil.FlagMeta{ + {FlagName: "body-param.bearer-token.header-name", FieldPath: "HeaderName", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Header name to inject the token into. Defaults to\n'Authorization'."}, + {FlagName: "body-param.bearer-token.id", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, + {FlagName: "body-param.bearer-token.prefix", FieldPath: "Prefix", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\nfor no prefix."}, + {FlagName: "body-param.bearer-token.token", FieldPath: "Token", Kind: flagutil.FlagKindString, Required: true, Description: "Required. Input only. The static bearer token. Write-only; never returned in responses. [required]"}, + }}, + {DiscriminatorValue: "oauth2", FlagName: "body-param.oauth2", FieldName: "OAuth2Config", CanExpand: false, Description: "OAuth2Config variant as JSON"}, + }}}, +} + +// initCreateCmd initializes the create command. +func initCreateCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "create", + Short: "Creates a credential.", + Long: "Creates a credential.", + Example: "", + Args: cobra.NoArgs, + RunE: runCreateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateCredential", + }, + } + flagutil.RegisterFlags(cmd, createCmdMeta) + if err := flagutil.ValidateMeta[operations.CreateCredentialRequest](createCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for create: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, createCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for create: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runCreateCmd executes the create command. +func runCreateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateCredential") + } + req, err := flagutil.BuildRequest[operations.CreateCredentialRequest](cmd, createCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Credentials.Create(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/credentials/delete.go b/internal/cli/credentials/delete.go new file mode 100644 index 0000000..7f99980 --- /dev/null +++ b/internal/cli/credentials/delete.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var deleteCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]"}, +} + +// initDeleteCmd initializes the delete command. +func initDeleteCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete", + Short: "Deletes a credential. Fails if referenced by active triggers.", + Long: "Deletes a credential. Fails if referenced by active triggers.", + Example: "", + Args: cobra.NoArgs, + RunE: runDeleteCmd, + Annotations: map[string]string{ + "speakeasy_operation": "DeleteCredential", + }, + } + flagutil.RegisterFlags(cmd, deleteCmdMeta) + if err := flagutil.ValidateMeta[operations.DeleteCredentialRequest](deleteCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for delete: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runDeleteCmd executes the delete command. +func runDeleteCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.DeleteCredentialRequest](cmd, deleteCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Credentials.Delete(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/credentials/get.go b/internal/cli/credentials/get.go new file mode 100644 index 0000000..b426681 --- /dev/null +++ b/internal/cli/credentials/get.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var getCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]"}, +} + +// initGetCmd initializes the get command. +func initGetCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Gets metadata of a single credential (no secret fields).", + Long: "Gets metadata of a single credential (no secret fields).", + Example: "", + Args: cobra.NoArgs, + RunE: runGetCmd, + Annotations: map[string]string{ + "speakeasy_operation": "GetCredential", + }, + } + flagutil.RegisterFlags(cmd, getCmdMeta) + if err := flagutil.ValidateMeta[operations.GetCredentialRequest](getCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for get: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runGetCmd executes the get command. +func runGetCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetCredentialRequest](cmd, getCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Credentials.Get(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/credentials/list.go b/internal/cli/credentials/list.go new file mode 100644 index 0000000..f4ea365 --- /dev/null +++ b/internal/cli/credentials/list.go @@ -0,0 +1,90 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listCmdMeta = []flagutil.FlagMeta{ + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. Maximum number of credentials to return.\nIf unspecified, defaults to 50. Maximum is 1000."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Pagination token."}, +} + +// initListCmd initializes the list command. +func initListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "Lists credentials for a project.", + Long: "Lists credentials for a project.", + Example: " gemini-api credentials list", + Args: cobra.NoArgs, + RunE: runListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ListCredentials", + }, + } + flagutil.RegisterFlags(cmd, listCmdMeta) + if err := flagutil.ValidateMeta[operations.ListCredentialsRequest](listCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runListCmd executes the list command. +func runListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.ListCredentialsRequest](cmd, listCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Credentials.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/credentials/root.go b/internal/cli/credentials/root.go new file mode 100644 index 0000000..2c8a349 --- /dev/null +++ b/internal/cli/credentials/root.go @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitCredentialsRoot(parent *cobra.Command) error { + var CredentialsCmd = &cobra.Command{ + Use: "credentials", + Short: "Operations for credentials", + Long: "Operations for credentials", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initListCmd(CredentialsCmd); err != nil { + return err + } + + if err := initCreateCmd(CredentialsCmd); err != nil { + return err + } + + if err := initDeleteCmd(CredentialsCmd); err != nil { + return err + } + + if err := initGetCmd(CredentialsCmd); err != nil { + return err + } + + if err := initUpdateCmd(CredentialsCmd); err != nil { + return err + } + + parent.AddCommand(CredentialsCmd) + return nil +} diff --git a/internal/cli/credentials/update.go b/internal/cli/credentials/update.go new file mode 100644 index 0000000..1f974fd --- /dev/null +++ b/internal/cli/credentials/update.go @@ -0,0 +1,110 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var updateCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]"}, + {FlagName: "update-mask", Shorthand: "u", FieldPath: "UpdateMask", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The list of fields to update."}, + {FlagName: "body-param", Shorthand: "b", FieldPath: "Body", Kind: flagutil.FlagKindUnion, Union: &flagutil.UnionMeta{Discriminated: true, DiscriminatorKey: "Type", TypeDescription: "JSON value (variants: environment_variable: { \"injection_location\": string | string[], \"trusted_domains\": string[], \"value\": string }, bearer_token: { \"header_name\": string, \"prefix\": string, \"token\": string }, oauth2: { \"client_id\": string, \"client_secret\": string, \"refresh_token\": string, \"scopes\": string[], ... })", Variants: []flagutil.UnionVariantMeta{ + {DiscriminatorValue: "environment_variable", FlagName: "body-param.environment-variable", FieldName: "EnvironmentVariableUpdateConfig", CanExpand: false, Description: "EnvironmentVariableUpdateConfig variant as JSON"}, + {DiscriminatorValue: "bearer_token", FlagName: "body-param.bearer-token", FieldName: "HTTPBearerUpdateConfig", CanExpand: true, Description: "HttpBearerUpdateConfig variant as JSON", Fields: []flagutil.FlagMeta{ + {FlagName: "body-param.bearer-token.header-name", FieldPath: "HeaderName", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Header name to inject the token into. Defaults to\n'Authorization'."}, + {FlagName: "body-param.bearer-token.prefix", FieldPath: "Prefix", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\nfor no prefix."}, + {FlagName: "body-param.bearer-token.token", FieldPath: "Token", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Input only. The static bearer token. Write-only; never returned in responses."}, + }}, + {DiscriminatorValue: "oauth2", FlagName: "body-param.oauth2", FieldName: "OAuth2UpdateConfig", CanExpand: false, Description: "OAuth2UpdateConfig variant as JSON"}, + }}}, +} + +// initUpdateCmd initializes the update command. +func initUpdateCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "update", + Short: "Updates a credential.", + Long: "Updates a credential.", + Example: "", + Args: cobra.NoArgs, + RunE: runUpdateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "UpdateCredential", + }, + } + flagutil.RegisterFlags(cmd, updateCmdMeta) + if err := flagutil.ValidateMeta[operations.UpdateCredentialRequest](updateCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for update: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, updateCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for update: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runUpdateCmd executes the update command. +func runUpdateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "UpdateCredential") + } + req, err := flagutil.BuildRequest[operations.UpdateCredentialRequest](cmd, updateCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Credentials.Update(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/custom/analyze.go b/internal/cli/custom/analyze.go new file mode 100644 index 0000000..2382d06 --- /dev/null +++ b/internal/cli/custom/analyze.go @@ -0,0 +1,140 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "errors" + "fmt" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/spf13/cobra" +) + +const defaultAnalyzeQuestion = "Describe this file in detail." + +// attachAnalyze turns the claimed analyze command into multimodal +// question-answering porcelain. Inputs and the question are deliberately +// separate: --input is repeatable media, while the positional is text. +func attachAnalyze(cmd *cobra.Command) { + cmd.Use = "analyze [question]" + cmd.Long = "Ask a question about one or more images, audio files, videos, PDFs, CSV or text\nfiles, or YouTube URLs.\n\nPass each media source separately with --input. files/ references use the\nFiles API, and YouTube URLs are passed by URI. The optional question applies to\nall inputs; its default is \"" + defaultAnalyzeQuestion + "\"\nBy default, stdout prints only the model's answer. The request is not stored\nserver-side.\n\n" + inlineLimitNote + cmd.Example = " gemini-api analyze -i report.pdf \"Summarize the key findings\"\n" + + " gemini-api analyze -i photo.jpg\n" + + " gemini-api analyze -i files/abc123 \"List every speaker with timestamps\"\n" + + " gemini-api analyze -i https://youtu.be/dQw4w9WgXcQ \"What happens at 1:00?\"\n" + + " gemini-api analyze -i a.png -i b.png \"What changed between these?\"" + helpMeta(cmd, "model "+defaultTextModel+" · question \""+defaultAnalyzeQuestion+"\"", + "https://ai.google.dev/gemini-api/docs/image-understanding (images) · https://ai.google.dev/gemini-api/docs/audio (audio) · https://ai.google.dev/gemini-api/docs/video-understanding (video) · https://ai.google.dev/gemini-api/docs/document-processing (documents)", + "full request control via gemini-api agent run") + cmd.Args = cobra.ArbitraryArgs + cmd.Flags().StringArrayP("input", "i", nil, "Local path, files/, or YouTube URL to analyze (repeatable)") + cmd.Flags().String("mime-type", "", "Override the detected MIME type (one input only)") + cmd.Flags().String("system", "", "System instruction to steer the analysis") + modelFlag(cmd, defaultTextModel, "text") + annotatePromptFlag(cmd, "input", flagutil.PromptFlagSpec{Required: true, Kind: "string-array", Order: 0}) + declareInteractive(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{{ + Name: "question", Summary: "Question to ask (default: \"" + defaultAnalyzeQuestion + "\")", Variadic: true, + }}}) + cmd.RunE = runAnalyze +} + +func runAnalyze(cmd *cobra.Command, args []string) error { + if usageRequested(cmd) { + return emitUsageKDL(cmd, cmd.OutOrStdout()) + } + inputs, _ := cmd.Flags().GetStringArray("input") + if len(inputs) == 0 && len(args) == 0 { + return output.UsageHelpError(cmd, errors.New("missing required flag --input (a local path, files/, or YouTube URL)")) + } + if len(inputs) == 0 { + return usageError("--input is required") + } + question := strings.TrimSpace(strings.Join(args, " ")) + if question == "" { + question = defaultAnalyzeQuestion + } + model, err := resolveModel(cmd, defaultTextModel) + if err != nil { + return err + } + s, sources, err := resolveMediaSources(cmd, inputs, analyzePolicy) + if err != nil { + return err + } + + contents := make([]interactions.Content, 0, len(sources)+1) + labels := make([]string, 0, len(sources)) + mimeTypes := make([]string, 0, len(sources)) + var inlineBytes int64 + for _, src := range sources { + block, err := src.block(cmd) + if err != nil { + return err + } + // Validation summed the sizes it saw; files may have grown since. + if inlineBytes += src.inlineBytes; inlineBytes > maxInlineBytes { + return usageError(fmt.Sprintf("%s: inline inputs grew past this CLI's %d MB request budget while the command ran", src.ref, maxInlineRequestBytes>>20)) + } + contents = append(contents, block) + labels = append(labels, src.label) + if src.mimeType != "" { + mimeTypes = append(mimeTypes, src.mimeType) + } + } + contents = append(contents, textContentBlock(question)) + + body := newModelInteraction(model, contents...) + if sys, _ := flagutil.GetStringFlag(cmd, "system"); strings.TrimSpace(sys) != "" { + body.SystemInstruction = stringPtr(strings.TrimSpace(sys)) + } + req := operations.CreateInteractionRequest{ + Body: operations.CreateCreateInteractionRequestBodyCreateModelInteraction(body), + } + + opts, err := callOpts(cmd) + if err != nil { + return err + } + if isDryRun(cmd) { + _, err := s.Agent.Run(cmd.Context(), req, opts...) + return err + } + + progress(cmd, "Analyzing %d input(s) with %s...", len(sources), model) + res, err := s.Agent.Run(cmd.Context(), req, opts...) + if err != nil { + return output.Error(cmd, err) + } + text, err := interactionText(res.Interaction) + if err != nil { + return err + } + text = strings.TrimRight(text, "\n") + envelope := map[string]any{ + "model": model, "inputs": labels, "question": question, "text": text, + } + if len(mimeTypes) > 0 { + envelope["mime_types"] = mimeTypes + } + if u := usageEnvelope(res.Interaction.Usage); u != nil { + envelope["usage"] = u + } + return emitResult(cmd, text, envelope) +} diff --git a/internal/cli/custom/custom_test.go b/internal/cli/custom/custom_test.go new file mode 100644 index 0000000..8053c1e --- /dev/null +++ b/internal/cli/custom/custom_test.go @@ -0,0 +1,481 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "encoding/binary" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/spf13/cobra" +) + +func TestParseTimestamp(t *testing.T) { + valid := map[string]int64{ + "7": 7000, + "7.25": 7250, + "01:02": 62000, + "01:02.5": 62500, + "01:02,500": 62500, + "1:00:00": 3600000, + "01:02:03.004": 3723004, + " 00:05 ": 5000, + "125.5": 125500, + "90:15": 5415000, + "01:02:03,250": 3723250, + } + for in, want := range valid { + got, err := parseTimestamp(in) + if err != nil || got != want { + t.Errorf("parseTimestamp(%q) = %d, %v; want %d", in, got, err, want) + } + } + for _, in := range []string{"", "soon", "1:2:3:4", "-1", "00:-5", "1::2", + "00:99", "00:60", "1.5:00", "00:00:61", "0:75:00", "1e3", "Inf", "0x10", "00: 05", "5."} { + if got, err := parseTimestamp(in); err == nil { + t.Errorf("parseTimestamp(%q) = %d, want an error", in, got) + } + } +} + +func TestRenderSRT(t *testing.T) { + segments := []transcriptSegment{ + {Speaker: "Speaker 1", StartTime: "00:00", EndTime: "00:02", Content: " Hello. "}, + // Crosstalk overlaps the previous segment; a zero-length caption is legal. + {Speaker: "Speaker 2", StartTime: "00:01", EndTime: "00:01", Content: "Hi."}, + } + got, err := renderSRT(segments, true) + want := "1\n00:00:00,000 --> 00:00:02,000\nSpeaker 1: Hello.\n\n" + + "2\n00:00:01,000 --> 00:00:01,000\nSpeaker 2: Hi.\n\n" + if err != nil || got != want { + t.Errorf("renderSRT = %q, %v; want %q", got, err, want) + } + + got, err = renderSRT(segments[:1], false) + if want := "1\n00:00:00,000 --> 00:00:02,000\nHello.\n\n"; err != nil || got != want { + t.Errorf("renderSRT without speakers = %q, %v; want %q", got, err, want) + } + + for wantErr, bad := range map[string][]transcriptSegment{ + "bad start_time": {{StartTime: "soon", EndTime: "00:01", Content: "x"}}, + "bad end_time": {{StartTime: "00:01", EndTime: "00:99", Content: "x"}}, + "is before start_time": {{StartTime: "00:05", EndTime: "00:04", Content: "x"}}, + "before the previous segment's": {{StartTime: "00:05", EndTime: "00:06", Content: "x"}, {StartTime: "00:04", EndTime: "00:07", Content: "y"}}, + } { + if got, err := renderSRT(bad, false); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("renderSRT(%+v) = %q, %v; want an error containing %q", bad, got, err, wantErr) + } + } +} + +func TestNormalizeFileID(t *testing.T) { + maxID := strings.Repeat("a", 40) + for in, wantID := range map[string]string{"files/abc-1": "abc-1", "a": "a", " files/abc ": "abc", maxID: maxID} { + name, id, ok := normalizeFileID(in) + if !ok || id != wantID || name != "files/"+wantID { + t.Errorf("normalizeFileID(%q) = %q, %q, %t; want files/%s", in, name, id, ok, wantID) + } + } + for _, in := range []string{"", "files/", "files/a/b", "a/../b", "files/a b", "files/a?b", + "files/ABC", "abc_2", "abc.x", "-abc", "abc-", maxID + "a"} { + if name, id, ok := normalizeFileID(in); ok { + t.Errorf("normalizeFileID(%q) = %q, %q; want rejection", in, name, id) + } + } +} + +func TestDetectMIME(t *testing.T) { + tests := []struct{ path, override, want string }{ + {"clip.MP3", "", "audio/mp3"}, + {"clip.mov", "", "video/mov"}, + {"doc.pdf", "", "application/pdf"}, + {"clip.mp3", " audio/wav ", "audio/wav"}, + {"clip.mp3", "Audio/WAV", "audio/wav"}, + {"scan.TIFF", "", "image/tiff"}, + {"clip.mkv", "", "video/x-matroska"}, + {"noext", "", ""}, + } + for _, tt := range tests { + if got := detectMIME(tt.path, tt.override); got != tt.want { + t.Errorf("detectMIME(%q, %q) = %q, want %q", tt.path, tt.override, got, tt.want) + } + } +} + +func TestContentClassOf(t *testing.T) { + tests := []struct { + mime string + wantClass contentClass + wantSendable bool + }{ + {"image/png", contentImage, true}, + {"image/svg+xml", contentImage, false}, + {"audio/mp3", contentAudio, true}, + {"audio/amr", contentAudio, false}, + {"video/mov", contentVideo, true}, + {"video/quicktime", contentVideo, false}, + {"video/x-matroska", contentVideo, false}, + {"application/pdf", contentDocument, true}, + // CSV is a document by enum even though it is textual. + {"text/csv", contentDocument, true}, + {"text/plain", contentText, true}, + {"text/x-go", contentText, true}, + {"application/json", contentText, true}, + {"application/rtf", contentDocument, false}, + {"application/octet-stream", contentDocument, false}, + {"", contentDocument, false}, + } + for _, tt := range tests { + if class, sendable := contentClassOf(tt.mime); class != tt.wantClass || sendable != tt.wantSendable { + t.Errorf("contentClassOf(%q) = %d, %t; want %d, %t", tt.mime, class, sendable, tt.wantClass, tt.wantSendable) + } + } +} + +// TestMIMETableIsSendable pins every curated extension to a MIME type the +// Interactions API accepts, so detection never yields a request it rejects. +func TestMIMETableIsSendable(t *testing.T) { + for ext, mimeType := range mimeByExtension { + if _, sendable := contentClassOf(mimeType); !sendable { + t.Errorf("mimeByExtension[%q] = %q is outside the interactions mime_type enums", ext, mimeType) + } + if ext != strings.ToLower(ext) || !strings.HasPrefix(ext, ".") { + t.Errorf("mimeByExtension key %q must be a lower-case extension", ext) + } + } + // An upload-only entry that became sendable belongs in mimeByExtension. + for ext, mimeType := range uploadOnlyMIME { + if _, sendable := contentClassOf(mimeType); sendable { + t.Errorf("uploadOnlyMIME[%q] = %q is sendable; move it to mimeByExtension", ext, mimeType) + } + if _, dup := mimeByExtension[ext]; dup { + t.Errorf("extension %q is in both MIME tables", ext) + } + } +} + +func TestIsYouTubeURL(t *testing.T) { + for _, in := range []string{"https://youtu.be/x", "https://www.youtube.com/watch?v=x", "http://m.youtube.com/watch?v=x", "https://music.youtube.com/watch?v=x"} { + if !isYouTubeURL(in) { + t.Errorf("isYouTubeURL(%q) = false", in) + } + } + for _, in := range []string{"youtu.be/x", "https://youtube.com.evil.example/x", "https://example.com/youtu.be", "ftp://youtu.be/x"} { + if isYouTubeURL(in) { + t.Errorf("isYouTubeURL(%q) = true", in) + } + } +} + +func TestMediaContentBlock(t *testing.T) { + tests := []struct { + mime string + wantType interactions.ContentType + }{ + {"image/png", interactions.ContentTypeImage}, + {"audio/mp3", interactions.ContentTypeAudio}, + {"video/mp4", interactions.ContentTypeVideo}, + {"application/pdf", interactions.ContentTypeDocument}, + {"", interactions.ContentTypeDocument}, + } + for _, tt := range tests { + for _, inline := range []bool{true, false} { + block := mediaContentBlock(tt.mime, "payload", inline) + if block.Type != tt.wantType { + t.Errorf("mediaContentBlock(%q).Type = %q, want %q", tt.mime, block.Type, tt.wantType) + } + var data, uri *string + switch { + case block.ImageContent != nil: + data, uri = block.ImageContent.Data, block.ImageContent.URI + case block.AudioContent != nil: + data, uri = block.AudioContent.Data, block.AudioContent.URI + case block.VideoContent != nil: + data, uri = block.VideoContent.Data, block.VideoContent.URI + case block.DocumentContent != nil: + data, uri = block.DocumentContent.Data, block.DocumentContent.URI + } + set, unset := data, uri + if !inline { + set, unset = uri, data + } + if set == nil || *set != "payload" || unset != nil { + t.Errorf("mediaContentBlock(%q, inline=%t): data=%v uri=%v", tt.mime, inline, data, uri) + } + } + } +} + +func TestTranscriptSchemaRequiredKeys(t *testing.T) { + tests := []struct { + speakers, timestamps bool + want []string + }{ + {true, true, []string{"content", "speaker", "start_time", "end_time"}}, + {false, true, []string{"content", "start_time", "end_time"}}, + {true, false, []string{"content", "speaker"}}, + {false, false, []string{"content"}}, + } + for _, tt := range tests { + schema := transcriptSchema(tt.speakers, tt.timestamps) + items := schema["properties"].(map[string]any)["segments"].(map[string]any)["items"].(map[string]any) + if got := items["required"].([]string); !reflect.DeepEqual(got, tt.want) { + t.Errorf("transcriptSchema(%t, %t) required = %v, want %v", tt.speakers, tt.timestamps, got, tt.want) + } + props := items["properties"].(map[string]any) + if len(props) != len(tt.want) { + t.Errorf("transcriptSchema(%t, %t) properties = %v, want exactly %v", tt.speakers, tt.timestamps, props, tt.want) + } + } +} + +func TestParseAudioMIME(t *testing.T) { + tests := []struct { + mime string + want audioFormat + }{ + {"audio/L16;codec=pcm;rate=24000", audioFormat{baseMIME: "audio/l16", sampleRate: 24000, channels: 1, isPCM: true}}, + {" audio/pcm ; Rate = 16000 ; channels=2 ", audioFormat{baseMIME: "audio/pcm", sampleRate: 16000, channels: 2, isPCM: true}}, + {"audio/l16", audioFormat{baseMIME: "audio/l16", channels: 1, isPCM: true}}, + // Unparseable or non-positive parameters are ignored, not trusted. + {"audio/l16;rate=fast;channels=0;codec", audioFormat{baseMIME: "audio/l16", channels: 1, isPCM: true}}, + // Compressed audio is never PCM and gets no channel default. + {"audio/mp3", audioFormat{baseMIME: "audio/mp3"}}, + {"audio/ogg;rate=48000", audioFormat{baseMIME: "audio/ogg", sampleRate: 48000}}, + {"", audioFormat{}}, + } + for _, tt := range tests { + if got := parseAudioMIME(tt.mime); got != tt.want { + t.Errorf("parseAudioMIME(%q) = %+v, want %+v", tt.mime, got, tt.want) + } + } +} + +func TestWavHeader(t *testing.T) { + tests := []struct { + pcmLen, sampleRate, channels int + wantByteRate uint32 + wantBlockAlign uint16 + }{ + {1000, 24000, 1, 48000, 2}, + {4, 48000, 2, 192000, 4}, + {0, 16000, 1, 32000, 2}, + } + for _, tt := range tests { + h := wavHeader(tt.pcmLen, tt.sampleRate, tt.channels) + if len(h) != 44 { + t.Fatalf("wavHeader length = %d, want 44", len(h)) + } + for offset, want := range map[int]string{0: "RIFF", 8: "WAVE", 12: "fmt ", 36: "data"} { + if got := string(h[offset : offset+4]); got != want { + t.Errorf("wavHeader[%d:] = %q, want %q", offset, got, want) + } + } + u16 := func(o int) uint16 { return binary.LittleEndian.Uint16(h[o:]) } + u32 := func(o int) uint32 { return binary.LittleEndian.Uint32(h[o:]) } + if u32(4) != uint32(36+tt.pcmLen) || u32(40) != uint32(tt.pcmLen) { + t.Errorf("RIFF/data sizes = %d/%d, want %d/%d", u32(4), u32(40), 36+tt.pcmLen, tt.pcmLen) + } + if u32(16) != 16 || u16(20) != 1 || u16(34) != 16 { + t.Errorf("fmt size/format/bits = %d/%d/%d, want 16/1 (PCM)/16", u32(16), u16(20), u16(34)) + } + if u16(22) != uint16(tt.channels) || u32(24) != uint32(tt.sampleRate) { + t.Errorf("channels/rate = %d/%d, want %d/%d", u16(22), u32(24), tt.channels, tt.sampleRate) + } + if u32(28) != tt.wantByteRate || u16(32) != tt.wantBlockAlign { + t.Errorf("byteRate/blockAlign = %d/%d, want %d/%d", u32(28), u16(32), tt.wantByteRate, tt.wantBlockAlign) + } + } +} + +func TestExtensionForAudioMIME(t *testing.T) { + for mime, want := range map[string]string{ + "audio/mp3": ".mp3", "audio/mpeg": ".mp3", "audio/aac": ".aac", "audio/ogg": ".ogg", + "audio/vorbis": ".ogg", "audio/flac": ".flac", "audio/opus": ".opus", "audio/m4a": ".m4a", + "audio/mp4": ".m4a", "AUDIO/MP3; rate=44100": ".mp3", + "audio/ogg_opus": ".ogg", + // Raw PCM is wrapped as WAV; a WAV response already is one. + "audio/l16;rate=24000": ".wav", "audio/wav": ".wav", + } { + if got, ok := extensionForAudioMIME(mime); !ok || got != want { + t.Errorf("extensionForAudioMIME(%q) = %q, %t; want %q", mime, got, ok, want) + } + } + // Headerless companded audio and unknown types have no playable form. + for _, mime := range []string{"audio/alaw", "audio/mulaw", "audio/l8", "audio/unknown", ""} { + if got, ok := extensionForAudioMIME(mime); ok { + t.Errorf("extensionForAudioMIME(%q) = %q, want it rejected", mime, got) + } + } +} + +// ttsCommand is a standalone tts command with the given flags parsed. +func ttsCommand(t *testing.T, flags ...string) *cobra.Command { + t.Helper() + cmd := &cobra.Command{Use: "tts"} + attachTTS(cmd) + if err := cmd.ParseFlags(flags); err != nil { + t.Fatalf("parsing %v: %v", flags, err) + } + return cmd +} + +func TestArtifactPath(t *testing.T) { + dir, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Chdir(dir) + + for out, want := range map[string]string{ + "speech.wav": filepath.Join(dir, "speech.wav"), + "speech.WAV": filepath.Join(dir, "speech.WAV"), + "speech": filepath.Join(dir, "speech.wav"), + "speech.mp3": filepath.Join(dir, "speech.wav"), + "v1.2/speech.mp3": filepath.Join(dir, "v1.2", "speech.wav"), + filepath.Join(dir, "abs", "a.b"): filepath.Join(dir, "abs", "a.wav"), + } { + got, err := artifactPath(ttsCommand(t, "--out", out), "out", "gemini-tts", ".wav") + if err != nil || got != want { + t.Errorf("artifactPath(--out %q) = %q, %v; want %q", out, got, err, want) + } + } + + defaultName := regexp.MustCompile(`^gemini-tts-\d+-[0-9a-f]{6}\.wav$`) + // A directory — by trailing separator or because it exists — receives the + // default-named file instead of becoming ".wav" or "/.wav". + if err := os.Mkdir(filepath.Join(dir, "existing"), 0o755); err != nil { + t.Fatal(err) + } + for out, wantDir := range map[string]string{ + "existing": filepath.Join(dir, "existing"), + "existing/": filepath.Join(dir, "existing"), + "fresh/": filepath.Join(dir, "fresh"), + } { + got, err := artifactPath(ttsCommand(t, "--out", out), "out", "gemini-tts", ".wav") + if err != nil || filepath.Dir(got) != wantDir || !defaultName.MatchString(filepath.Base(got)) { + t.Errorf("artifactPath(--out %q) = %q, %v; want %s/gemini-tts--.wav", out, got, err, wantDir) + } + } + + seen := map[string]bool{} + for _, flags := range [][]string{nil, {"--out", " "}, nil} { + got, err := artifactPath(ttsCommand(t, flags...), "out", "gemini-tts", ".wav") + if err != nil || filepath.Dir(got) != dir || !defaultName.MatchString(filepath.Base(got)) { + t.Errorf("artifactPath(%v) = %q, %v; want %s/gemini-tts--.wav", flags, got, err, dir) + } + if seen[got] { + t.Errorf("artifactPath default %q repeated", got) + } + seen[got] = true + } +} + +func TestParseTranscript(t *testing.T) { + full := `{"segments":[{"speaker":"Speaker 1","start_time":"00:00","end_time":"00:02","content":"Hello."}]}` + want := []transcriptSegment{{Speaker: "Speaker 1", StartTime: "00:00", EndTime: "00:02", Content: "Hello."}} + for name, raw := range map[string]string{ + "bare": full, + "padded": "\n " + full + "\n", + "json fence": "```json\n" + full + "\n```", + "anonymous fence": "```\n" + full + "\n```", + } { + got, err := parseTranscript(raw, true, true) + if err != nil || !reflect.DeepEqual(got, want) { + t.Errorf("parseTranscript(%s) = %+v, %v; want %+v", name, got, err, want) + } + } + + // A missing speaker is labelled rather than rejected, and only when asked for. + noSpeaker := `{"segments":[{"content":"Hi.","start_time":"0","end_time":"1"}]}` + if got, err := parseTranscript(noSpeaker, true, true); err != nil || got[0].Speaker != "Speaker" { + t.Errorf("parseTranscript(speakers on) = %+v, %v; want the placeholder speaker", got, err) + } + if got, err := parseTranscript(noSpeaker, false, true); err != nil || got[0].Speaker != "" { + t.Errorf("parseTranscript(speakers off) = %+v, %v; want no speaker", got, err) + } + // Timestamps are only required when requested. + if _, err := parseTranscript(`{"segments":[{"content":"Hi."}]}`, false, false); err != nil { + t.Errorf("parseTranscript(timestamps off) rejected a bare segment: %v", err) + } + + for name, tt := range map[string]struct{ raw, wantErr string }{ + "not json": {"Speaker 1: hello", "not valid JSON"}, + "no segments": {`{"segments":[]}`, "no segments"}, + "wrong shape": {`{"transcript":"hello"}`, "no segments"}, + "blank content": {`{"segments":[{"content":" ","start_time":"0","end_time":"1"}]}`, "segment 1 has no content"}, + "missing end": {`{"segments":[{"content":"a","start_time":"0","end_time":"1"},{"content":"b","start_time":"1"}]}`, "segment 2 is missing start_time/end_time"}, + "unclosed fence": {"```json\n" + full[:len(full)-1], "not valid JSON"}, + } { + if got, err := parseTranscript(tt.raw, true, true); err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("parseTranscript(%s) = %+v, %v; want an error containing %q", name, got, err, tt.wantErr) + } + } +} + +func TestBuildSpeechConfig(t *testing.T) { + speech := func(speaker, voice, language string) interactions.SpeechConfig { + c := interactions.SpeechConfig{Voice: stringPtr(voice)} + if speaker != "" { + c.Speaker = stringPtr(speaker) + } + if language != "" { + c.Language = stringPtr(language) + } + return c + } + single := []struct { + flags []string + want interactions.SpeechConfig + }{ + {nil, speech("", defaultTTSVoice, "")}, + {[]string{"--voice", " Puck ", "--language", " en-US "}, speech("", "Puck", "en-US")}, + {[]string{"--voice", " "}, speech("", defaultTTSVoice, "")}, + {[]string{"--voice", "Puck", "--multi-speaker", " "}, speech("", "Puck", "")}, + } + for _, tt := range single { + got, label, err := buildSpeechConfig(ttsCommand(t, tt.flags...)) + if err != nil || label != *tt.want.Voice || got.SpeakerConfig != nil || !reflect.DeepEqual(got.ArrayOfSpeechConfig, []interactions.SpeechConfig{tt.want}) { + t.Errorf("buildSpeechConfig(%v) = %+v, %v; want the single voice %+v", tt.flags, got, err, tt.want) + } + } + + got, label, err := buildSpeechConfig(ttsCommand(t, "--multi-speaker", " Alice = Kore ,, Bob=Puck, ", "--language", "en-GB")) + wantSpeakers := []interactions.SpeechConfig{speech("Alice", "Kore", "en-GB"), speech("Bob", "Puck", "en-GB")} + if err != nil || label != "multi-speaker Alice = Kore ,, Bob=Puck," || got.ArrayOfSpeechConfig != nil || got.SpeakerConfig == nil || !reflect.DeepEqual(got.SpeakerConfig.Speakers, wantSpeakers) { + t.Errorf("buildSpeechConfig(multi-speaker) = %+v, %v; want speakers %+v", got, err, wantSpeakers) + } + + for wantErr, flags := range map[string][]string{ + "mutually exclusive": {"--voice", "Puck", "--multi-speaker", "A=Kore"}, + `invalid --multi-speaker entry "A"`: {"--multi-speaker", "A"}, + `entry "A="`: {"--multi-speaker", "A="}, + `entry "=Kore"`: {"--multi-speaker", "B=Puck,=Kore"}, + "exactly two Speaker=Voice entries (got 0)": {"--multi-speaker", " , ,"}, + "exactly two Speaker=Voice entries (got 1)": {"--multi-speaker", "A=Kore"}, + "exactly two Speaker=Voice entries (got 3)": {"--multi-speaker", "A=Kore,B=Puck,C=Zephyr"}, + `speaker "A" more than once`: {"--multi-speaker", "A=Kore,A=Puck"}, + `speaker "A B" more than once`: {"--multi-speaker", "A B=Kore, A B =Puck"}, + } { + if got, _, err := buildSpeechConfig(ttsCommand(t, flags...)); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("buildSpeechConfig(%v) = %+v, %v; want an error containing %q", flags, got, err, wantErr) + } + } +} diff --git a/internal/cli/custom/files_upload.go b/internal/cli/custom/files_upload.go new file mode 100644 index 0000000..4294ab3 --- /dev/null +++ b/internal/cli/custom/files_upload.go @@ -0,0 +1,384 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/spf13/cobra" +) + +// uploadChunkSize is the resumable-upload chunk size (8 MiB), matching the +// official SDK. Only the final chunk carries the "finalize" command. +const uploadChunkSize = 8 << 20 + +// newFilesUploadCmd builds the "files upload" command. Upload uses the +// resumable /upload//files protocol (X-Goog-Upload-* headers), which +// is absent from the OpenAPI document, so it is hand-written over the CLI's +// shared runtime transport rather than a generated operation. +func newFilesUploadCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "upload ", + Short: "Upload a local file to the Files API (48h TTL)", + Long: "Upload a local file and return its reusable files/ name. Use that name with\n\"gemini-api analyze\", \"transcribe\", or advanced interaction requests. Uploaded\nfiles expire after 48 hours; the service accepts up to 2 GB per file (50 MB for\nPDFs) and 20 GB per project.\n\nBy default, stdout prints only the files/ name.\n\nArguments:\n Local file to upload", + Example: " gemini-api files upload lecture.mp4 # → files/abc123\n" + + " gemini-api files upload photo.png --display-name \"Cover\"\n" + + " gemini-api files upload clip.wav --wait # wait until ready", + Args: cobra.MaximumNArgs(1), + RunE: runFilesUpload, + } + helpMeta(cmd, "MIME type detected from the extension", + "https://ai.google.dev/gemini-api/docs/files", + "metadata-only registration via gemini-api files register") + cmd.Flags().String("display-name", "", "Human-readable display name for the file") + cmd.Flags().String("mime-type", "", "Override the detected MIME type") + cmd.Flags().Bool("wait", false, "Wait until the uploaded file is ready for use") + cmd.Flags().Duration("wait-timeout", 5*time.Minute, "Maximum time to wait when --wait is set") + cmd.Flags().Lookup("wait-timeout").DefValue = "5m" + declareInteractive(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{{ + Name: "path", Summary: "Local file to upload", Required: true, + }}}) + return cmd +} + +func runFilesUpload(cmd *cobra.Command, args []string) error { + if usageRequested(cmd) { + return emitUsageKDL(cmd, cmd.OutOrStdout()) + } + if len(args) == 0 { + return output.UsageHelpError(cmd, errors.New("missing required argument (a local file to upload)")) + } + path := args[0] + source, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return usageError(fmt.Sprintf("file not found: %s", path)) + } + return usageError(fmt.Sprintf("cannot read file: %s", path)) + } + defer source.Close() + info, err := source.Stat() + if err != nil { + return usageError(fmt.Sprintf("cannot inspect file: %s", path)) + } + if !info.Mode().IsRegular() { + return usageError(fmt.Sprintf("not a regular file: %s", path)) + } + if info.Size() == 0 { + return usageError(fmt.Sprintf("file is empty: %s", path)) + } + mimeOverride, _ := flagutil.GetStringFlag(cmd, "mime-type") + mimeType := detectMIME(path, mimeOverride) + if mimeType == "" { + return usageError(fmt.Sprintf("cannot determine the MIME type of %s", path), + "Pass --mime-type explicitly (for example --mime-type video/mp4)") + } + displayName, _ := flagutil.GetStringFlag(cmd, "display-name") + if strings.TrimSpace(displayName) == "" { + displayName = filepath.Base(path) + } + + t, err := newRawTransport(cmd) + if err != nil { + return err + } + + // Step 1: start the resumable session. + meta := map[string]any{"file": map[string]any{"display_name": displayName}} + metaBytes, _ := json.Marshal(meta) + startURL := t.apiURL("upload/" + t.apiVersion + "/files") + startReq, err := t.newRequest(cmd.Context(), http.MethodPost, startURL, bytes.NewReader(metaBytes)) + if err != nil { + return err + } + startReq.Header.Set("Content-Type", "application/json") + startReq.Header.Set("X-Goog-Upload-Protocol", "resumable") + startReq.Header.Set("X-Goog-Upload-Command", "start") + startReq.Header.Set("X-Goog-Upload-Header-Content-Length", strconv.FormatInt(info.Size(), 10)) + startReq.Header.Set("X-Goog-Upload-Header-Content-Type", mimeType) + + if isDryRun(cmd) { + startRes, err := t.do(startReq) + if err != nil { + return err + } + startRes.Body.Close() + // The real session URL is response-dependent. A stable URL derived + // from the start endpoint lets the preview show every exact chunk. + placeholderURL := strings.TrimRight(startURL, "/") + "/dry-run-session" + if err := t.previewUploadChunks(cmd.Context(), placeholderURL, source, path, info.Size()); err != nil { + return err + } + if wait, _ := flagutil.GetBoolFlag(cmd, "wait"); wait { + progress(cmd, "[DRY-RUN] --wait polling is response-dependent and was not simulated.") + } + return nil + } + + progress(cmd, "Starting upload of %s (%d bytes, %s)...", path, info.Size(), mimeType) + startRes, err := t.do(startReq) + if err != nil { + return output.Error(cmd, err) + } + uploadURL := startRes.Header.Get("X-Goog-Upload-Url") + io.Copy(io.Discard, startRes.Body) + startRes.Body.Close() + if uploadURL == "" { + return runtimeError("the server did not return an upload URL") + } + if !t.sameService(uploadURL) { + // Name the host only: the session URL carries the upload_id capability. + host := uploadURL + if parsed, err := url.Parse(uploadURL); err == nil && parsed.Host != "" { + host = parsed.Scheme + "://" + parsed.Host + } + return runtimeError(fmt.Sprintf("refusing to upload to an unexpected host: %s", host)) + } + + // Step 2: stream the bytes in chunks; only the last chunk finalizes. + file, err := t.uploadBytes(cmd.Context(), uploadURL, source, path, info.Size()) + if err != nil { + if r, ok := err.(rawErr); ok { + return output.Error(cmd, r.err) + } + return runtimeError(err.Error()) + } + if file.Name == nil || *file.Name == "" { + return runtimeError("upload finished but the response had no file name") + } + name := *file.Name + progress(cmd, "Uploaded %s as %s.", path, name) + + if wait, _ := flagutil.GetBoolFlag(cmd, "wait"); wait { + waited, err := t.waitActive(cmd, name) + if err != nil { + return err + } + if waited != nil { + file = *waited + } + } + return emitResult(cmd, name, fileEnvelope(&file)) +} + +// previewUploadChunks reuses the source handle opened before the start request +// but does not read it. The known file size is enough to emit the same offsets, +// lengths, commands, and binary body markers that the live loop would send. +func (t *rawTransport) previewUploadChunks(ctx context.Context, uploadURL string, source *os.File, path string, size int64) error { + if _, err := source.Stat(); err != nil { + return fmt.Errorf("cannot inspect %s: %w", path, err) + } + + for offset := int64(0); offset < size; { + n := int64(uploadChunkSize) + if remaining := size - offset; remaining < n { + n = remaining + } + command := "upload" + if offset+n == size { + command = "upload, finalize" + } + req, err := t.newRequest(ctx, http.MethodPost, uploadURL, client.NewDryRunBody(n)) + if err != nil { + return err + } + req.ContentLength = n + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Length", strconv.FormatInt(n, 10)) + req.Header.Set("X-Goog-Upload-Command", command) + req.Header.Set("X-Goog-Upload-Offset", strconv.FormatInt(offset, 10)) + res, err := t.do(req) + if err != nil { + return err + } + res.Body.Close() + offset += n + } + return nil +} + +// rawErr marks an error that already carries an SDK-classified API error so +// the caller routes it through output.Error. +type rawErr struct{ err error } + +func (r rawErr) Error() string { return r.err.Error() } + +// uploadBytes streams the already-open source to the resumable session URL in +// chunks. Exactly the declared size is sent: bytes appended after the start +// request are ignored, and a source that shrank fails before finalizing. +func (t *rawTransport) uploadBytes(ctx context.Context, uploadURL string, source io.Reader, path string, size int64) (genai.File, error) { + f := io.LimitReader(source, size) + buf := make([]byte, uploadChunkSize) + var offset int64 + var lastBody []byte + for { + n, readErr := io.ReadFull(f, buf) + // "finalize" must ride the last chunk. Detect the end by byte count + // (offset+n == size), not just io.EOF: a file whose size is an exact + // multiple of the chunk size fills the buffer with readErr == nil on + // its final chunk, so an EOF-only check would send that chunk as a + // plain "upload" and never finalize. + final := false + switch { + case readErr == nil: + final = offset+int64(n) >= size + case readErr == io.EOF || readErr == io.ErrUnexpectedEOF: + if offset+int64(n) != size { + return genai.File{}, fmt.Errorf("%s changed during upload: read %d of %d bytes", path, offset+int64(n), size) + } + final = true + default: + return genai.File{}, fmt.Errorf("reading %s at offset %d: %w", path, offset, readErr) + } + command := "upload" + if final { + command = "upload, finalize" + } + req, err := t.newRequest(ctx, http.MethodPost, uploadURL, bytes.NewReader(buf[:n])) + if err != nil { + return genai.File{}, err + } + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Length", strconv.Itoa(n)) + req.Header.Set("X-Goog-Upload-Command", command) + req.Header.Set("X-Goog-Upload-Offset", strconv.FormatInt(offset, 10)) + res, err := t.do(req) + if err != nil { + return genai.File{}, rawErr{err} + } + body, readBodyErr := io.ReadAll(res.Body) + status := res.Header.Get("X-Goog-Upload-Status") + res.Body.Close() + if readBodyErr != nil { + return genai.File{}, fmt.Errorf("reading the upload response at offset %d: %w", offset, readBodyErr) + } + lastBody = body + offset += int64(n) + if final { + if status != "final" { + return genai.File{}, fmt.Errorf("upload finalized but server status is %q", status) + } + break + } + if status != "active" { + return genai.File{}, fmt.Errorf("upload interrupted: server status is %q at offset %d", status, offset) + } + } + + var wrapper struct { + File genai.File `json:"file"` + } + if err := json.Unmarshal(lastBody, &wrapper); err != nil { + return genai.File{}, fmt.Errorf("upload finished but the response was not valid JSON: %w", err) + } + return wrapper.File, nil +} + +// waitActive polls files.get until the file is ACTIVE, fails, or the +// --wait-timeout deadline passes; the last sleep is cut to that deadline. +func (t *rawTransport) waitActive(cmd *cobra.Command, name string) (*genai.File, error) { + s, err := client.NewClient(cmd) + if err != nil { + return nil, err + } + callOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return nil, err + } + timeout, _ := cmd.Flags().GetDuration("wait-timeout") + deadline := time.Now().Add(timeout) + id := strings.TrimPrefix(name, "files/") + delay := 2 * time.Second + for { + res, err := s.Files.FilesGet(cmd.Context(), operations.FilesGetRequest{File: id}, callOpts...) + if err != nil { + return nil, output.Error(cmd, err) + } + file := res.File + if file != nil && file.State != nil { + switch *file.State { + case genai.StateActive: + progress(cmd, "%s is ACTIVE.", name) + return file, nil + case genai.StateFailed: + return nil, runtimeError(fmt.Sprintf("%s failed processing on the server", name)) + case genai.StateProcessing, genai.StateStateUnspecified, "": + default: + // A state this CLI does not know will not turn into ACTIVE by waiting. + return nil, runtimeError(fmt.Sprintf("%s is in unexpected state %s", name, *file.State), + fmt.Sprintf("Check it: gemini-api files get %s", name)) + } + } + remaining := time.Until(deadline) + if remaining <= 0 { + return nil, runtimeError(fmt.Sprintf("%s did not become ACTIVE within %s", name, timeout), + fmt.Sprintf("Check its state later: gemini-api files get %s", name)) + } + progress(cmd, "Waiting for %s to become ACTIVE...", name) + select { + case <-cmd.Context().Done(): + return nil, cmd.Context().Err() + case <-time.After(min(delay, remaining)): + } + if delay < 15*time.Second { + delay += 2 * time.Second + } + } +} + +// fileEnvelope flattens a File into the porcelain JSON envelope. +func fileEnvelope(f *genai.File) map[string]any { + env := map[string]any{} + if f == nil { + return env + } + if f.Name != nil { + env["name"] = *f.Name + } + if f.URI != nil { + env["uri"] = *f.URI + } + if f.MimeType != nil { + env["mime_type"] = *f.MimeType + } + if f.DisplayName != nil { + env["display_name"] = *f.DisplayName + } + if f.SizeBytes != nil { + env["size_bytes"] = *f.SizeBytes + } + if f.State != nil { + env["state"] = string(*f.State) + } + return env +} diff --git a/internal/cli/custom/hardening_test.go b/internal/cli/custom/hardening_test.go new file mode 100644 index 0000000..3d7b1f5 --- /dev/null +++ b/internal/cli/custom/hardening_test.go @@ -0,0 +1,207 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" +) + +func TestSameService(t *testing.T) { + transport := &rawTransport{baseURL: "https://generativelanguage.googleapis.com"} + local := &rawTransport{baseURL: "http://127.0.0.1:8080"} + cases := []struct { + t *rawTransport + url string + want bool + }{ + {transport, "https://generativelanguage.googleapis.com/upload/v1beta/files?upload_id=x", true}, + {transport, "https://upload.googleapis.com/session", true}, + {transport, "https://googleapis.com/session", true}, + {transport, "HTTPS://Generativelanguage.GoogleApis.com/session", true}, + {transport, "http://generativelanguage.googleapis.com/session", false}, + {transport, "https://googleapis.com.evil.example/session", false}, + {transport, "https://evilgoogleapis.com/session", false}, + {transport, "https://googleapis.com@evil.example/session", false}, + {transport, "https://evil.example/?h=.googleapis.com", false}, + {transport, "/upload/v1beta/files", false}, + {transport, "", false}, + {transport, "://bad", false}, + {local, "http://127.0.0.1:8080/session", true}, + {local, "http://127.0.0.1:9090/session", false}, + {local, "https://127.0.0.1:8080/session", false}, + {local, "http://localhost:8080/session", false}, + } + for _, tc := range cases { + if got := tc.t.sameService(tc.url); got != tc.want { + t.Errorf("sameService(%q) with base %s = %v, want %v", tc.url, tc.t.baseURL, got, tc.want) + } + } +} + +func TestInlineCost(t *testing.T) { + if got := inlineCost(3<<20, contentAudio); got != 4<<20 { + t.Errorf("3 MiB of audio costs %d, want 4 MiB as base64", got) + } + if got := inlineCost(3<<20, contentText); got != 3<<20 { + t.Errorf("3 MiB of text costs %d, want it unchanged", got) + } + // The largest accepted media file still fits the request budget encoded. + largest := int64(maxInlineBytes) / 4 * 3 + if cost := inlineCost(largest, contentVideo); cost > maxInlineBytes || cost+inlineRequestReserve > maxInlineRequestBytes { + t.Errorf("largest media file costs %d, over the %d budget", cost, maxInlineBytes) + } +} + +func TestCheckLocalFileInlineBoundary(t *testing.T) { + largest := int64(maxInlineBytes) / 4 * 3 + for name, tc := range map[string]struct { + file string + size int64 + wantOK bool + }{ + "media at the cap": {"a.mp4", largest, true}, + "media over the cap": {"b.mp4", largest + 3, false}, + "text at the cap": {"a.txt", maxInlineBytes, true}, + "text over the cap": {"b.txt", maxInlineBytes + 1, false}, + } { + path := filepath.Join(t.TempDir(), tc.file) + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + // Sparse: sized without writing the bytes. + if err := f.Truncate(tc.size); err != nil { + t.Fatal(err) + } + f.Close() + _, _, err = checkLocalFile(path, "", "--input[1]", analyzePolicy) + if (err == nil) != tc.wantOK { + t.Errorf("%s: checkLocalFile error = %v, want ok=%v", name, err, tc.wantOK) + } + } +} + +func TestCanonicalRemoteMIME(t *testing.T) { + for in, want := range map[string]string{ + "video/quicktime": "video/mov", + "Video/QuickTime; codecs=x": "video/mov", + "audio/mp3": "audio/mp3", + " audio/WAV ;rate=1": "audio/wav", + "": "", + } { + if got := canonicalRemoteMIME(in); got != want { + t.Errorf("canonicalRemoteMIME(%q) = %q, want %q", in, got, want) + } + } +} + +func TestInteractionOutcome(t *testing.T) { + rejected := []interactions.InteractionStatus{ + interactions.InteractionStatusIncomplete, interactions.InteractionStatusBudgetExceeded, + interactions.InteractionStatusFailed, interactions.InteractionStatusCancelled, + interactions.InteractionStatusRequiresAction, interactions.InteractionStatusInProgress, + interactions.InteractionStatusQueued, + } + for _, status := range rejected { + err := interactionOutcome(&interactions.Interaction{Status: status}) + if err == nil || !strings.Contains(err.Error(), string(status)) { + t.Errorf("status %q: error = %v, want it rejected by name", status, err) + } + } + for _, status := range []interactions.InteractionStatus{interactions.InteractionStatusCompleted, "", "future_state"} { + if err := interactionOutcome(&interactions.Interaction{Status: status}); err != nil { + t.Errorf("status %q: unexpected error %v", status, err) + } + } + if err := interactionOutcome(nil); err == nil { + t.Error("a nil interaction was accepted") + } +} + +func TestRenderSRTKeepsCuesIntact(t *testing.T) { + segments := []transcriptSegment{ + {StartTime: "00:00", EndTime: "00:02", Content: "first line\n\n \nsecond line --> still text"}, + {StartTime: "00:02", EndTime: "00:04", Content: "next"}, + } + srt, err := renderSRT(segments, false) + if err != nil { + t.Fatal(err) + } + if cues := strings.Split(strings.TrimSpace(srt), "\n\n"); len(cues) != len(segments) { + t.Errorf("rendered %d cues from %d segments:\n%s", len(cues), len(segments), srt) + } + if !strings.Contains(srt, "first line\nsecond line --> still text") { + t.Errorf("cue text was altered beyond blank lines:\n%s", srt) + } +} + +func FuzzTimestampRoundTrip(f *testing.F) { + for _, seed := range []int64{0, 999, 62500, 3723004, 359999999} { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, ms int64) { + if ms < 0 || ms > 1<<40 { + t.Skip() + } + got, err := parseTimestamp(formatSRTTime(ms)) + if err != nil || got != ms { + t.Errorf("parseTimestamp(formatSRTTime(%d)) = %d, %v", ms, got, err) + } + }) +} + +func FuzzIdentifierNormalizers(f *testing.F) { + for _, seed := range []string{"files/abc-123", "models/gemini-2.5-flash", "../x", "files/", "a/b", "%2e%2e", " x "} { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, in string) { + for name, normalize := range map[string]func(string) (string, error){ + "file": normalizeFilePositional, "model": normalizeModelPositional, + } { + id, err := normalize(in) + if err != nil { + continue + } + if id == "" || id == "." || id == ".." || strings.ContainsAny(id, "/\\%?# ") { + t.Errorf("%s normalizer accepted %q as %q, not a plain path segment", name, in, id) + } + } + }) +} + +func FuzzAudioMIME(f *testing.F) { + for _, seed := range []string{"audio/l16; rate=24000; channels=1", "audio/L16;rate=;channels=x", ";;;", "audio/mp3"} { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, in string) { + parseAudioMIME(in) + extensionForAudioMIME(in) + }) +} + +func TestUploadBytesSendsTheDeclaredSize(t *testing.T) { + // A source that shrank below the declared size fails before finalizing. + transport := &rawTransport{} + _, err := transport.uploadBytes(t.Context(), "http://127.0.0.1:1/session", bytes.NewReader(nil), "gone.bin", 10) + if err == nil || !strings.Contains(err.Error(), "changed during upload") { + t.Errorf("error = %v, want the shrunken source named", err) + } +} diff --git a/internal/cli/custom/media.go b/internal/cli/custom/media.go new file mode 100644 index 0000000..f1b2d70 --- /dev/null +++ b/internal/cli/custom/media.go @@ -0,0 +1,605 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "encoding/base64" + "errors" + "fmt" + "io/fs" + "mime" + "net/url" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "unicode/utf8" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk" + "github.com/spf13/cobra" +) + +// maxInlineRequestBytes is the CLI's conservative budget for one interaction +// request that inlines local files; inlineRequestReserve is the share kept for +// the prompt, response schema, and JSON framing. Inputs are charged at their +// encoded size (see inlineCost). Larger inputs go through the Files API +// ("gemini-api files upload") and are referenced by files/. +const ( + maxInlineRequestBytes = 20 << 20 + inlineRequestReserve = 1 << 20 + maxInlineBytes = maxInlineRequestBytes - inlineRequestReserve +) + +// inlineCost is what a local file of the given size adds to the request body: +// text travels as is, everything else as base64 (4/3 of the raw size). +func inlineCost(size int64, class contentClass) int64 { + if class == contentText { + return size + } + return int64(base64.StdEncoding.EncodedLen(int(size))) +} + +// inlineLimitNote is the help sentence analyze and transcribe share. +var inlineLimitNote = fmt.Sprintf("Local files are sent inline; this CLI keeps each request under %d MB, base64\nincluded (about %d MB of media). Upload larger files with \"gemini-api files upload\"\nand pass the returned files/.", + maxInlineRequestBytes>>20, (maxInlineBytes/4*3)>>20) + +// mimeByExtension maps common media/document extensions to the MIME types the +// Interactions API accepts. It is consulted before the platform's mime database +// so results are stable across machines. Media entries use the spellings of the +// interactions mime_type enums (video/mov, not video/quicktime); every entry +// must classify as sendable (see contentClassOf), which the unit tests pin. +var mimeByExtension = map[string]string{ + ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", + ".gif": "image/gif", ".heic": "image/heic", ".heif": "image/heif", ".bmp": "image/bmp", + ".tif": "image/tiff", ".tiff": "image/tiff", + ".mp3": "audio/mp3", ".wav": "audio/wav", ".ogg": "audio/ogg", ".m4a": "audio/m4a", + ".flac": "audio/flac", ".aac": "audio/aac", ".opus": "audio/opus", ".aiff": "audio/aiff", + ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/mov", ".avi": "video/avi", + ".mpeg": "video/mpeg", ".mpg": "video/mpeg", ".flv": "video/x-flv", + ".wmv": "video/wmv", ".3gp": "video/3gpp", + ".pdf": "application/pdf", ".csv": "text/csv", + ".txt": "text/plain", ".md": "text/markdown", + ".html": "text/html", ".htm": "text/html", ".xml": "text/xml", ".json": "application/json", + ".js": "text/javascript", ".ts": "text/x-typescript", + ".py": "text/x-python", ".go": "text/x-go", ".css": "text/css", ".yaml": "text/yaml", ".yml": "text/yaml", +} + +// uploadOnlyMIME holds extensions the Files API stores but no interactions +// content block accepts: "files upload" still detects them, while analyze and +// transcribe reject them by name instead of as an unknown type. +var uploadOnlyMIME = map[string]string{ + ".amr": "audio/amr", ".wma": "audio/x-ms-wma", ".mkv": "video/x-matroska", ".rtf": "application/rtf", +} + +// detectMIME returns the lower-cased MIME type for a local path: explicit +// override first, then the curated tables, then the platform database. Empty +// when unknown. +func detectMIME(path, override string) string { + if strings.TrimSpace(override) != "" { + return strings.ToLower(strings.TrimSpace(override)) + } + ext := strings.ToLower(filepath.Ext(path)) + if m, ok := mimeByExtension[ext]; ok { + return m + } + if m, ok := uploadOnlyMIME[ext]; ok { + return m + } + if m := mime.TypeByExtension(ext); m != "" { + // Drop parameters such as "; charset=utf-8" — the API wants the bare type. + if i := strings.Index(m, ";"); i >= 0 { + m = strings.TrimSpace(m[:i]) + } + return strings.ToLower(m) + } + return "" +} + +// contentClass is the interactions content block a MIME type travels in. +type contentClass int + +const ( + contentDocument contentClass = iota + contentImage + contentAudio + contentVideo + contentText +) + +// textApplicationMIME lists the application/* types that are plain text and so +// travel as a text block alongside text/*. +var textApplicationMIME = map[string]bool{ + "application/json": true, "application/xml": true, "application/yaml": true, + "application/x-yaml": true, "application/javascript": true, +} + +// contentClassOf picks the content block for a MIME type and reports whether +// the API accepts that exact type there. The image, audio, video, and document +// blocks each publish a closed mime_type enum (documents are only PDF and CSV); +// every other textual type is sent as a text block, which carries no MIME type. +// Anything else falls to the document block with sendable=false. +func contentClassOf(mimeType string) (class contentClass, sendable bool) { + switch { + case strings.HasPrefix(mimeType, "image/"): + mt := interactions.ImageContentMimeType(mimeType) + return contentImage, mt.IsExact() + case strings.HasPrefix(mimeType, "audio/"): + mt := interactions.AudioContentMimeType(mimeType) + return contentAudio, mt.IsExact() + case strings.HasPrefix(mimeType, "video/"): + mt := interactions.VideoContentMimeType(mimeType) + return contentVideo, mt.IsExact() + } + if mt := interactions.DocumentContentMimeType(mimeType); mt.IsExact() { + return contentDocument, true + } + if strings.HasPrefix(mimeType, "text/") || textApplicationMIME[mimeType] { + return contentText, true + } + return contentDocument, false +} + +// mediaPolicy is what one porcelain command accepts as an --input. +type mediaPolicy struct { + allowYouTube bool + enforceCumulative bool + // classes limits the accepted content classes; nil accepts every sendable one. + classes []contentClass + // accepts names those classes in the rejection ("audio or video"), and + // hint points at the command that takes the rest. + accepts string + hint string +} + +var ( + analyzePolicy = mediaPolicy{allowYouTube: true, enforceCumulative: true} + transcribePolicy = mediaPolicy{ + classes: []contentClass{contentAudio, contentVideo}, + accepts: "audio or video", + hint: "Use \"gemini-api analyze\" for images, documents, and text", + } +) + +// checkClass rejects a MIME type whose content class the command does not take. +func (p mediaPolicy) checkClass(mimeType, ref string) error { + if p.classes == nil { + return nil + } + class, _ := contentClassOf(mimeType) + if slices.Contains(p.classes, class) { + return nil + } + return usageError(fmt.Sprintf("%s: only %s inputs are accepted; got %s", ref, p.accepts, mimeType), p.hint) +} + +// mediaSource is the resolved input of analyze/transcribe: a request content +// block plus a human label for progress lines and artifact names. +type mediaSource struct { + // content is the ready block of a URI-backed source (files/, YouTube). + content interactions.Content + // path is set instead for a local file, which block reads on demand. + path string + ref string + label string + mimeType string + // inlineBytes is what block found the local file to cost once read. + inlineBytes int64 +} + +// block returns the interaction content block. A local file is read and +// base64-encoded here rather than at resolution, so a multi-input transcribe +// holds one payload at a time instead of all of them. +func (m *mediaSource) block(cmd *cobra.Command) (interactions.Content, error) { + if m.path == "" { + return m.content, nil + } + class, _ := contentClassOf(m.mimeType) + var data []byte + if isDryRun(cmd) { + // No bytes leave the machine under --dry-run; keep the preview + // readable instead of streaming the payload to stderr. + info, err := os.Stat(m.path) + if err != nil { + return interactions.Content{}, usageError(fmt.Sprintf("%s: cannot read file: %v", m.ref, err)) + } + data = fmt.Appendf(nil, "", info.Size()) + } else { + raw, err := os.ReadFile(m.path) + if err != nil { + return interactions.Content{}, usageError(fmt.Sprintf("%s: cannot read file: %v", m.ref, err)) + } + // The file may have grown since validation sized it. + m.inlineBytes = inlineCost(int64(len(raw)), class) + if m.inlineBytes > maxInlineBytes { + return interactions.Content{}, usageError(fmt.Sprintf("%s: file grew past the inline limit while the command ran", m.ref)) + } + data = raw + if class != contentText { + data = base64.StdEncoding.AppendEncode(nil, raw) + } + } + if class == contentText { + return textContentBlock(string(data)), nil + } + return mediaContentBlock(m.mimeType, string(data), true), nil +} + +// mediaContentBlock builds an interactions Content block for a media input, +// choosing the union member by MIME class. payload is base64 bytes when inline, +// or a URI otherwise. A Files API resource of unknown type travels as a +// document block without a MIME type. +func mediaContentBlock(mimeType, payload string, inline bool) interactions.Content { + set := func(data, uri **string) { + if inline { + *data = stringPtr(payload) + } else { + *uri = stringPtr(payload) + } + } + switch class, _ := contentClassOf(mimeType); class { + case contentImage: + c := interactions.ImageContent{MimeType: interactions.ImageContentMimeType(mimeType).ToPointer()} + set(&c.Data, &c.URI) + return interactions.CreateContentImage(c) + case contentAudio: + c := interactions.AudioContent{MimeType: interactions.AudioContentMimeType(mimeType).ToPointer()} + set(&c.Data, &c.URI) + return interactions.CreateContentAudio(c) + case contentVideo: + c := interactions.VideoContent{MimeType: interactions.VideoContentMimeType(mimeType).ToPointer()} + set(&c.Data, &c.URI) + return interactions.CreateContentVideo(c) + default: + c := interactions.DocumentContent{} + if mimeType != "" { + c.MimeType = interactions.DocumentContentMimeType(mimeType).ToPointer() + } + set(&c.Data, &c.URI) + return interactions.CreateContentDocument(c) + } +} + +var ( + youtubeHost = regexp.MustCompile(`^(www\.|m\.|music\.)?(youtube\.com|youtu\.be)$`) + // fileIDShape is the Files API id contract: up to 40 lowercase + // alphanumerics or dashes, not led or ended by a dash. It is a single path + // segment, so "." and ".." never reach the /files/{file} path. + fileIDShape = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`) +) + +// isYouTubeURL reports whether the argument is one of the YouTube URL forms +// the Interactions API accepts as a video content URI. +func isYouTubeURL(arg string) bool { + if !strings.HasPrefix(arg, "http://") && !strings.HasPrefix(arg, "https://") { + return false + } + u, err := url.Parse(arg) + if err != nil { + return false + } + return youtubeHost.MatchString(strings.ToLower(u.Host)) +} + +// normalizeFileID accepts "files/" or a bare id and returns both the +// canonical resource name and the bare id. Returns ok=false for anything +// that does not look like a Files API identifier. +func normalizeFileID(arg string) (name, id string, ok bool) { + arg = strings.TrimSpace(arg) + id = strings.TrimPrefix(arg, "files/") + if id == "" || !fileIDShape.MatchString(id) { + return "", "", false + } + return "files/" + id, id, true +} + +// inputRef is the stable label used by validation and API errors. Keeping the +// one-based position visible matters when a repeated --input fails. +func inputRef(index int, arg string) string { + return fmt.Sprintf("--input[%d] %q", index, arg) +} + +// mediaInputKind is what an analyze/transcribe --input value refers to. +type mediaInputKind int + +const ( + mediaInputLocal mediaInputKind = iota + mediaInputRemoteFile + mediaInputYouTube +) + +// classifyMediaInput decides what one --input refers to without touching the +// network. An existing path is always local, even when its spelling starts with +// files/; only an absent, explicit files/ value is a Files API reference, +// and bare Files API ids are deliberately not guessed. +func classifyMediaInput(arg, ref string, allowYouTube bool) (mediaInputKind, error) { + if arg == "" { + return 0, usageError(ref + ": input cannot be empty") + } + if isYouTubeURL(arg) { + if !allowYouTube { + return 0, usageError(ref+": YouTube URLs are not supported by transcribe", + "Download the media locally or upload it with \"gemini-api files upload \"") + } + return mediaInputYouTube, nil + } + if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + return 0, usageError(ref+": only YouTube URLs are supported as remote inputs", + "Download the file locally, or upload it with \"gemini-api files upload \" and pass files/") + } + _, statErr := os.Stat(arg) + if statErr == nil { + return mediaInputLocal, nil + } + if errors.Is(statErr, fs.ErrPermission) { + return 0, usageError(fmt.Sprintf("%s: cannot access file: %v", ref, statErr)) + } + if !strings.HasPrefix(arg, "files/") { + return 0, usageError(ref + ": file not found; for an uploaded file pass files/") + } + if _, _, ok := normalizeFileID(arg); !ok { + return 0, usageError(ref + ": invalid Files API reference; expected files/") + } + return mediaInputRemoteFile, nil +} + +// checkLocalFile enforces what an inline input must satisfy — a non-empty +// regular file within the inline cap whose MIME type the API and the command +// accept — and returns its inline cost and MIME type. +func checkLocalFile(path, mimeOverride, ref string, policy mediaPolicy) (int64, string, error) { + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return 0, "", usageError(ref + ": file not found") + } + return 0, "", usageError(fmt.Sprintf("%s: cannot access file: %v", ref, err)) + } + if !info.Mode().IsRegular() { + return 0, "", usageError(ref + ": not a regular file") + } + if info.Size() == 0 { + return 0, "", usageError(ref + ": file is empty") + } + mimeType := detectMIME(path, mimeOverride) + if mimeType == "" { + return 0, "", usageError(ref+": cannot determine the MIME type", + "Pass --mime-type explicitly (for example --mime-type audio/mp3)") + } + class, sendable := contentClassOf(mimeType) + if !sendable { + return 0, "", usageError(fmt.Sprintf("%s: MIME type %s is not accepted by the Interactions API", ref, mimeType), + "Supported inputs: images, audio, video, PDF, CSV, and text files; convert the file, or pass --mime-type if the detection is wrong") + } + if err := policy.checkClass(mimeType, ref); err != nil { + return 0, "", err + } + cost := inlineCost(info.Size(), class) + if cost > maxInlineBytes && class == contentText { + // Text travels inline only; an uploaded copy could not be referenced. + return 0, "", usageError(fmt.Sprintf("%s: text file is %d bytes; this CLI sends text inline, up to %d MB per request", ref, info.Size(), maxInlineBytes>>20)) + } + if cost > maxInlineBytes { + return 0, "", usageError( + fmt.Sprintf("%s: file is %d bytes (%d as base64); this CLI keeps inline requests under %d MB", ref, info.Size(), cost, maxInlineRequestBytes>>20), + fmt.Sprintf("Run \"gemini-api files upload %s\" and pass the returned files/ instead", path)) + } + return cost, mimeType, nil +} + +// probeLocalFile confirms the file is readable. A file bound for a text block +// is read in full, since only its content tells text from mislabelled binary. +func probeLocalFile(path, mimeType, ref string) error { + if class, _ := contentClassOf(mimeType); class == contentText { + data, err := os.ReadFile(path) + if err != nil { + return usageError(fmt.Sprintf("%s: cannot read file: %v", ref, err)) + } + if !utf8.Valid(data) { + return usageError(fmt.Sprintf("%s: detected as %s but the content is not UTF-8 text", ref, mimeType), + "Pass --mime-type with the file's real media type") + } + return nil + } + f, err := os.Open(path) + if err != nil { + return usageError(fmt.Sprintf("%s: cannot read file: %v", ref, err)) + } + return f.Close() +} + +// prevalidateMediaInputs validates every local input before any response- +// dependent Files API lookup can run. This preserves all-or-nothing probing: +// a bad later path cannot occur after an earlier remote request was previewed +// or sent. Probing each file also catches permissions and mislabelled text. +func prevalidateMediaInputs(inputs []string, mimeOverride string, policy mediaPolicy) error { + var inlineBytes int64 + for i, raw := range inputs { + arg := strings.TrimSpace(raw) + ref := inputRef(i+1, arg) + kind, err := classifyMediaInput(arg, ref, policy.allowYouTube) + if err != nil { + return err + } + if kind != mediaInputLocal { + continue + } + cost, mimeType, err := checkLocalFile(arg, mimeOverride, ref, policy) + if err != nil { + return err + } + if err := probeLocalFile(arg, mimeType, ref); err != nil { + return err + } + inlineBytes += cost + if policy.enforceCumulative && inlineBytes > maxInlineBytes { + return usageError( + fmt.Sprintf("%s: cumulative inline inputs (base64 included) exceed this CLI's %d MB request budget", ref, maxInlineRequestBytes>>20), + fmt.Sprintf("Run \"gemini-api files upload %s\" and pass the returned files/ instead", arg)) + } + } + return nil +} + +// resolveMediaSources is the shared front half of analyze/transcribe: it +// validates every --input, builds the client, and resolves each input in order. +func resolveMediaSources(cmd *cobra.Command, inputs []string, policy mediaPolicy) (*sdk.GeminiAPI, []*mediaSource, error) { + mimeOverride, _ := flagutil.GetStringFlag(cmd, "mime-type") + if len(inputs) > 1 && strings.TrimSpace(mimeOverride) != "" { + return nil, nil, usageError("--mime-type can only be used with exactly one --input") + } + if err := prevalidateMediaInputs(inputs, mimeOverride, policy); err != nil { + return nil, nil, err + } + s, err := client.NewClient(cmd) + if err != nil { + return nil, nil, err + } + sources := make([]*mediaSource, 0, len(inputs)) + for i, input := range inputs { + src, err := resolveMediaSource(cmd, s, input, mimeOverride, i+1, policy) + if err != nil { + return nil, nil, err + } + sources = append(sources, src) + } + return s, sources, nil +} + +// resolveMediaSource turns one analyze/transcribe --input into a request +// part. Existing regular local files win, explicit files/ references are +// resolved through files.get, and analyze may additionally allow YouTube +// URLs. +func resolveMediaSource(cmd *cobra.Command, s *sdk.GeminiAPI, arg, mimeOverride string, index int, policy mediaPolicy) (*mediaSource, error) { + arg = strings.TrimSpace(arg) + ref := inputRef(index, arg) + kind, err := classifyMediaInput(arg, ref, policy.allowYouTube) + if err != nil { + return nil, err + } + switch kind { + case mediaInputYouTube: + return &mediaSource{ + content: interactions.CreateContentVideo(interactions.VideoContent{URI: stringPtr(arg)}), + label: arg, + }, nil + case mediaInputRemoteFile: + name, id, _ := normalizeFileID(arg) + return resolveRemoteFile(cmd, s, name, id, mimeOverride, ref, policy) + } + _, mimeType, err := checkLocalFile(arg, mimeOverride, ref, policy) + if err != nil { + return nil, err + } + abs, _ := filepath.Abs(arg) + return &mediaSource{path: arg, ref: ref, label: abs, mimeType: mimeType}, nil +} + +// resolveRemoteFile looks a Files API resource up so the request part carries +// its URI and MIME type. Under --dry-run the lookup is previewed and a +// placeholder URI is used so the main request can still be previewed. +func resolveRemoteFile(cmd *cobra.Command, s *sdk.GeminiAPI, name, id, mimeOverride, ref string, policy mediaPolicy) (*mediaSource, error) { + fileURI := "https://generativelanguage.googleapis.com/v1beta/" + name + mimeType := strings.ToLower(strings.TrimSpace(mimeOverride)) + if err := checkRemoteMIME(mimeType, ref, policy); err != nil { + return nil, err // a bad override fails before the lookup is sent + } + // The lookup honors --header and is previewed like every other call; its + // synthetic dry-run response carries no metadata, so the placeholder URI + // stands in. + opts, err := callOpts(cmd) + if err != nil { + return nil, err + } + res, err := s.Files.FilesGet(cmd.Context(), operations.FilesGetRequest{File: id}, opts...) + if err != nil { + progress(cmd, "%s: request failed", ref) + return nil, output.Error(cmd, err) + } + if isDryRun(cmd) { + progress(cmd, "[DRY-RUN] %s: the file's URI and MIME type come from the files.get response; the previewed block is a placeholder.", ref) + } else { + if res.File == nil { + return nil, runtimeError(fmt.Sprintf("%s: the Files API returned no metadata", ref)) + } + if res.File.URI != nil && *res.File.URI != "" { + fileURI = *res.File.URI + } + if mimeType == "" && res.File.MimeType != nil { + mimeType = canonicalRemoteMIME(*res.File.MimeType) + } + state := genai.State("") + if res.File.State != nil { + state = *res.File.State + } + switch state { + case genai.StateActive: + case genai.StateProcessing: + return nil, runtimeError(fmt.Sprintf("%s: %s is still processing", ref, name), + fmt.Sprintf("Wait for it to become ACTIVE: gemini-api files get %s", name)) + case genai.StateFailed: + return nil, runtimeError(fmt.Sprintf("%s: %s failed processing on the server", ref, name)) + default: + if state == "" { + state = "unset" + } + return nil, runtimeError(fmt.Sprintf("%s: %s is not ACTIVE (state: %s)", ref, name, state), + fmt.Sprintf("Check it: gemini-api files get %s", name)) + } + if mimeType == "" && policy.classes != nil { + return nil, runtimeError(fmt.Sprintf("%s: the Files API reports no MIME type for %s, so it cannot be confirmed as %s", ref, name, policy.accepts), + "Pass --mime-type explicitly (for example --mime-type audio/mp3)") + } + } + if err := checkRemoteMIME(mimeType, ref, policy); err != nil { + return nil, err + } + return &mediaSource{content: mediaContentBlock(mimeType, fileURI, false), label: name, mimeType: mimeType}, nil +} + +// checkRemoteMIME rejects a known MIME type no URI-backed block accepts, or the +// command does not take. A text block cannot reference a URI, so only the typed +// media and document blocks can carry a Files API resource; an unknown ("") +// type is left to the caller. +func checkRemoteMIME(mimeType, ref string, policy mediaPolicy) error { + if mimeType == "" { + return nil + } + if class, sendable := contentClassOf(mimeType); !sendable || class == contentText { + return usageError(fmt.Sprintf("%s: MIME type %s cannot be referenced by URI in the Interactions API", ref, mimeType), + "Supported uploaded inputs: images, audio, video, PDF, and CSV; pass text files as local paths, or --mime-type if the stored type is wrong") + } + return policy.checkClass(mimeType, ref) +} + +// remoteMIMEAliases maps the canonical spellings the Files API may report to +// the ones the interactions mime_type enums publish. +var remoteMIMEAliases = map[string]string{"video/quicktime": "video/mov"} + +// canonicalRemoteMIME lower-cases a Files API MIME type, drops its parameters, +// and folds known aliases onto the interactions spelling. +func canonicalRemoteMIME(mimeType string) string { + mimeType, _, _ = strings.Cut(strings.ToLower(mimeType), ";") + mimeType = strings.TrimSpace(mimeType) + if alias, ok := remoteMIMEAliases[mimeType]; ok { + return alias + } + return mimeType +} diff --git a/internal/cli/custom/porcelain.go b/internal/cli/custom/porcelain.go new file mode 100644 index 0000000..58a9427 --- /dev/null +++ b/internal/cli/custom/porcelain.go @@ -0,0 +1,485 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +// Shared plumbing for the hand-written tier-1 porcelain commands (tts, analyze, +// transcribe, files upload). Everything here leans on the +// generated CLI's exported packages so the porcelain inherits the same +// credential resolution, --server-url, --timeout, --dry-run, --debug, output +// formatting, and agent-mode error envelope as the generated commands. + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +// Curated model defaults for the porcelain commands. They mirror the Build +// Spec's tier-1 line-up and the reference CLI's model registry. +const ( + defaultTextModel = "gemini-3.6-flash" + defaultTranscribeModel = "gemini-3.6-flash" + defaultTTSModel = "gemini-3.1-flash-tts-preview" + defaultTTSVoice = "Kore" +) + +// helpMeta feeds the generated compact help template: the "Defaults:" line +// (rendered between the examples and the flags) and the "Learn: … · escalate: +// …" footer line. The template itself adds the machine-interface and +// --help-global footer lines to every command, so porcelain never embeds +// footer prose in cmd.Example. +func helpMeta(cmd *cobra.Command, defaults, learn, escalate string) { + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + for key, value := range map[string]string{ + "speakeasy_help_defaults": defaults, + "speakeasy_help_learn": learn, + "speakeasy_help_escalate": escalate, + } { + if value != "" { + cmd.Annotations[key] = value + } + } +} + +// porcelainError is a CLI-originated failure carrying remediation lines. The +// generated output layer classifies it (output.Classify) and renders it once +// in whichever mode the caller asked for — the classified human diagnostic, +// the JSON envelope, or agent mode — so porcelain commands never print +// themselves; they only return. +type porcelainError struct { + error + hints []string +} + +func (e porcelainError) CLIHints() []string { return e.hints } + +func (e porcelainError) Unwrap() error { return e.error } + +// usageError reports a CLI-level usage/validation problem: typed at this +// boundary as a usage failure (validation_error / CLI_VALIDATION, exit 2). +// The shared classifier already appends the --help/--usage pointer, so hints +// carry only what is specific to the failure. +func usageError(msg string, hints ...string) error { + return porcelainError{error: flagutil.WithCLIValidation(errors.New(msg)), hints: hints} +} + +// runtimeError reports a failure the CLI detected after a successful API +// exchange (empty audio, blocked response, an upload that never became +// ACTIVE, ...). Untyped, so the shared classifier files it as a runtime +// failure (exit 1) with the supplied hints; the message names what went wrong. +func runtimeError(msg string, hints ...string) error { + return porcelainError{error: errors.New(msg), hints: hints} +} + +// isDryRun reports whether --dry-run is active. Under dry-run the HTTP layer +// prints the would-be request to stderr and returns a synthetic empty +// response, so commands must return before interpreting response bodies. +func isDryRun(cmd *cobra.Command) bool { + return client.IsDryRun(cmd) +} + +// callOpts prepares the SDK call options shared by every porcelain request. +// Under --dry-run the synthetic empty response is never deserialized. +func callOpts(cmd *cobra.Command) ([]operations.Option, error) { + opts, err := output.PrepareCallOpts(cmd) + if err != nil { + return nil, err + } + if isDryRun(cmd) { + opts = append(opts, operations.WithSkipDeserialization()) + } + return opts, nil +} + +// progress prints a status line to stderr unless the caller asked for a +// machine-readable output format (spec §7: stderr is silent on success under +// the JSON envelope). +func progress(cmd *cobra.Command, format string, args ...any) { + if wantsEnvelope(cmd) { + return + } + fmt.Fprintf(cmd.ErrOrStderr(), format+"\n", args...) +} + +// effectiveOutputFormat mirrors the generated output package's format +// resolution (flag > env/config > agent-mode default) but returns "" when +// none names a format: the porcelain then prints only the deliverable on +// stdout, which is what a human driving the command wants by default. Agent +// mode selects the structured envelope (TOON), matching the CLI-wide +// --agent-mode contract and the generated commands. +func effectiveOutputFormat(cmd *cobra.Command) string { + if flagutil.FlagChanged(cmd, "output-format") { + format, _ := flagutil.GetStringFlag(cmd, "output-format") + return format + } + if val := config.GetString("output-format"); val != "" { + return val + } + if output.IsAgentMode() { + return "toon" + } + return "" +} + +// wantsEnvelope reports whether the caller asked for the structured envelope +// (an explicit output format or a --jq expression) instead of the bare +// deliverable. +func wantsEnvelope(cmd *cobra.Command) bool { + if effectiveOutputFormat(cmd) != "" { + return true + } + return flagutil.FlagChanged(cmd, "jq") +} + +// emitResult writes the command result. Deliverable-only mode prints the +// bare deliverable (text, integer, or absolute path) followed by a newline; +// any explicit --output-format or --jq renders the small snake_case envelope +// through the generated output package so json/yaml/toon/table and jq all +// behave exactly as on generated commands. +func emitResult(cmd *cobra.Command, deliverable string, envelope map[string]any) error { + if isDryRun(cmd) { + return nil + } + if !wantsEnvelope(cmd) { + _, err := fmt.Fprintln(cmd.OutOrStdout(), deliverable) + return err + } + if effectiveOutputFormat(cmd) == "pretty" { + // Pretty is the human format: keep the deliverable itself, the + // envelope is for machines. + jq, _ := flagutil.GetStringFlag(cmd, "jq") + if jq == "" { + _, err := fmt.Fprintln(cmd.OutOrStdout(), deliverable) + return err + } + } + return output.Result(cmd, envelope) +} + +// modelFlag registers the shared -m/--model override with its curated default. +func modelFlag(cmd *cobra.Command, def, route string) { + cmd.Flags().StringP("model", "m", "", fmt.Sprintf("Override model (default: %s) — %s-capable models: https://ai.google.dev/gemini-api/docs/models", def, route)) +} + +// resolveModel returns the --model override or the curated default, stripped +// of any "models/" prefix (the SDK path parameter wants the bare id). It +// holds the override to the same id shape as "models get". +func resolveModel(cmd *cobra.Command, def string) (string, error) { + model, _ := flagutil.GetStringFlag(cmd, "model") + if strings.TrimSpace(model) == "" { + model = def + } + id, err := normalizeModelPositional(model) + if err != nil { + return "", usageError("--model: " + err.Error()) + } + return id, nil +} + +// stringPtr / boolPtr are tiny helpers for the SDK's pointer-heavy request +// models. +func stringPtr(s string) *string { return &s } +func boolPtr(b bool) *bool { return &b } + +// textInput resolves a single text input from exactly one of: positional args, +// a -- file, or -- stdin. More than one source is a usage +// error; the returned text is trimmed and non-empty (or "" when none supplied). +func textInput(cmd *cobra.Command, args []string, fileFlag, stdinFlag string) (string, error) { + sources := 0 + if len(args) > 0 { + sources++ + } + filePath := "" + if fileFlag != "" && flagutil.FlagChanged(cmd, fileFlag) { + filePath, _ = flagutil.GetStringFlag(cmd, fileFlag) + sources++ + } + fromStdin := false + if stdinFlag != "" { + fromStdin, _ = flagutil.GetBoolFlag(cmd, stdinFlag) + if fromStdin { + sources++ + } + } + if sources > 1 { + return "", usageError("provide the text once: as an argument, via --" + fileFlag + ", or via --" + stdinFlag + " (not several)") + } + switch { + case filePath != "": + data, err := os.ReadFile(filePath) + if err != nil { + return "", usageError(fmt.Sprintf("cannot read --%s %q: %v", fileFlag, filePath, err)) + } + text := strings.TrimSpace(string(data)) + if text == "" { + return "", usageError(fmt.Sprintf("--%s %q is empty", fileFlag, filePath)) + } + return text, nil + case fromStdin: + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return "", usageError(fmt.Sprintf("cannot read stdin: %v", err)) + } + text := strings.TrimSpace(string(data)) + if text == "" { + return "", usageError("stdin is empty") + } + return text, nil + case len(args) > 0: + text := strings.TrimSpace(strings.Join(args, " ")) + if text == "" { + return "", usageError("text cannot be empty") + } + return text, nil + } + return "", nil +} + +// textContentBlock builds a user text content block for an interaction input. +func textContentBlock(text string) interactions.Content { + return interactions.CreateContentText(interactions.TextContent{Text: text}) +} + +// modelPtr wraps a bare model id in the interactions Model pointer the request +// models expect. +func modelPtr(model string) *interactions.Model { + m := interactions.Model(model) + return &m +} + +// newModelInteraction is the request every porcelain command starts from: one +// non-streaming turn that is not stored server-side. The porcelain is a +// one-shot transformation — nothing continues or manages the interaction +// afterwards — so retaining the prompt, media, and output would serve no one. +func newModelInteraction(model string, content ...interactions.Content) interactions.CreateModelInteraction { + return interactions.CreateModelInteraction{ + Model: modelPtr(model), + Stream: boolPtr(false), + Store: boolPtr(false), + Input: interactions.CreateInteractionsInputArrayOfContent(content), + } +} + +// interactionOutcome rejects an interaction whose status is a known +// non-success value, so partial or halted output is never delivered as a +// complete artifact. An omitted status, or one this CLI does not know, is +// accepted: the content checks that follow still fail closed on empty output. +func interactionOutcome(it *interactions.Interaction) error { + if it == nil { + return runtimeError("empty response from the API") + } + detail := "" + if msg := firstInteractionError(it); msg != "" { + detail = ": " + msg + } + switch it.Status { + case interactions.InteractionStatusIncomplete, interactions.InteractionStatusBudgetExceeded: + return runtimeError(fmt.Sprintf("the API stopped before the output was complete (status: %s)%s", it.Status, detail), + "Nothing was written; shorten or split the input, or raise the limit via gemini-api agent run") + case interactions.InteractionStatusFailed, interactions.InteractionStatusCancelled, + interactions.InteractionStatusRequiresAction, interactions.InteractionStatusInProgress, + interactions.InteractionStatusQueued: + return runtimeError(fmt.Sprintf("the interaction did not complete (status: %s)%s", it.Status, detail)) + } + return nil +} + +// interactionText concatenates the model's text output from a completed +// (non-streaming) interaction. It reports platform errors, non-success +// statuses, and empty responses as errors so callers never print a partial or +// empty deliverable and exit 0. +func interactionText(it *interactions.Interaction) (string, error) { + if err := interactionOutcome(it); err != nil { + return "", err + } + // Prefer the top-level output_text when the API still returns it; current + // revisions surface output only through the model-output steps below. + if it.OutputText != nil && strings.TrimSpace(*it.OutputText) != "" { + return *it.OutputText, nil + } + var sb strings.Builder + for _, step := range it.Steps { + if step.ModelOutputStep == nil { + continue + } + for _, block := range step.ModelOutputStep.Content { + if block.TextContent != nil { + sb.WriteString(block.TextContent.Text) + } + } + } + text := sb.String() + if strings.TrimSpace(text) == "" { + if msg := firstInteractionError(it); msg != "" { + return "", runtimeError("the API returned no text: "+msg, + "Rephrase the request or supply different input") + } + msg := "the API returned no text" + if it.Status != "" { + msg += " (status: " + string(it.Status) + ")" + } + return "", runtimeError(msg) + } + return text, nil +} + +// firstInteractionError returns the first platform error message recorded on an +// interaction, if any. +func firstInteractionError(it *interactions.Interaction) string { + for _, e := range it.Errors { + if e.Message != nil && strings.TrimSpace(*e.Message) != "" { + return *e.Message + } + } + return "" +} + +// usageEnvelope flattens the interaction's usage metadata into the porcelain +// envelope. Nil when the API reported nothing. +func usageEnvelope(u *interactions.Usage) map[string]any { + if u == nil { + return nil + } + m := map[string]any{} + if u.TotalInputTokens != nil { + m["prompt_tokens"] = *u.TotalInputTokens + } + if u.TotalOutputTokens != nil { + m["output_tokens"] = *u.TotalOutputTokens + } + if u.TotalThoughtTokens != nil { + m["thoughts_tokens"] = *u.TotalThoughtTokens + } + if u.TotalTokens != nil { + m["total_tokens"] = *u.TotalTokens + } + if len(m) == 0 { + return nil + } + return m +} + +// artifactPath resolves where a generated artifact lands: --out when given, +// otherwise ./-- in the working directory. An +// --out naming a directory (trailing separator, or an existing directory) +// receives that default-named file. The returned path is absolute (that is +// what stdout carries). +func artifactPath(cmd *cobra.Command, outFlag, prefix, ext string) (string, error) { + out, _ := flagutil.GetStringFlag(cmd, outFlag) + out = strings.TrimSpace(out) + if out == "" || namesDirectory(out) { + var rnd [3]byte + _, _ = rand.Read(rnd[:]) + out = filepath.Join(out, fmt.Sprintf("%s-%d-%s%s", prefix, time.Now().UnixMilli(), hex.EncodeToString(rnd[:]), ext)) + } else if !strings.EqualFold(filepath.Ext(out), ext) { + // Keep the artifact honest: the extension follows the bytes. + if filepath.Ext(out) == "" { + out += ext + } else { + out = strings.TrimSuffix(out, filepath.Ext(out)) + ext + } + } + abs, err := filepath.Abs(out) + if err != nil { + return "", usageError(fmt.Sprintf("cannot resolve --%s %q: %v", outFlag, out, err)) + } + return abs, nil +} + +// namesDirectory reports whether an --out value refers to a directory rather +// than the artifact file itself: a trailing separator, or an existing directory. +func namesDirectory(out string) bool { + if strings.HasSuffix(out, "/") || strings.HasSuffix(out, string(os.PathSeparator)) { + return true + } + info, err := os.Stat(out) + return err == nil && info.IsDir() +} + +// writeArtifact writes data atomically (temp file + rename) creating parent +// directories as needed. An existing destination is replaced, and the file +// keeps os.CreateTemp's 0600 mode: artifacts may hold private speech or text. +func writeArtifact(path string, data []byte) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp") + if err != nil { + return err + } + tmpName := tmp.Name() + if _, err := tmp.Write(data); err != nil { + tmp.Close() + os.Remove(tmpName) + return err + } + if err := tmp.Close(); err != nil { + os.Remove(tmpName) + return err + } + if err := os.Rename(tmpName, path); err != nil { + os.Remove(tmpName) + return err + } + return nil +} + +// emitUsageKDL delegates to the generated tree-wide usage contract. Claimed +// commands are marked dynamic, so the live Cobra surface and interactive +// argument declaration are the source of truth. +func emitUsageKDL(cmd *cobra.Command, w io.Writer) error { + return usage.EmitSchema(cmd, w) +} + +// usageRequested is the shared --usage gate: it wins over every other surface. +func usageRequested(cmd *cobra.Command) bool { + return usage.UsageRequested(cmd) +} + +// declareInteractive attaches the generated CLI's typed positional contract +// to hand-written porcelain. Registration-time errors are programming errors, +// so fail fast instead of silently losing prompting and live usage metadata. +func declareInteractive(cmd *cobra.Command, spec interactive.CommandSpec) { + if err := interactive.Declare(cmd, spec); err != nil { + panic(fmt.Sprintf("declare interactive inputs for %s: %v", cmd.CommandPath(), err)) + } +} + +// annotatePromptFlag attaches the generated CLI's flag prompt contract with +// the same fail-fast semantics as declareInteractive. +func annotatePromptFlag(cmd *cobra.Command, name string, spec flagutil.PromptFlagSpec) { + if err := flagutil.AnnotatePromptFlag(cmd, name, spec); err != nil { + panic(fmt.Sprintf("declare interactive flag --%s for %s: %v", name, cmd.CommandPath(), err)) + } +} diff --git a/internal/cli/custom/rawhttp.go b/internal/cli/custom/rawhttp.go new file mode 100644 index 0000000..04fa2dd --- /dev/null +++ b/internal/cli/custom/rawhttp.go @@ -0,0 +1,356 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk" + "github.com/google-gemini/gemini-api-cli/internal/testclient" + "github.com/spf13/cobra" +) + +// rawTransport sends hand-built HTTP requests (today: the resumable Files +// upload protocol, which the OpenAPI document does not describe). It mirrors +// the generated client's global options from the generated CLI's exported +// helpers: the single credential config.PickCredential selects, a validated +// server URL, API version, Api-Revision, quota project, --header values, +// test-client injection, and the --dry-run / --debug diagnostics wrapper. +// +// Deliberate differences from the generated client: SDK request hooks do not +// run, --timeout bounds each request end to end (headers and body) instead of +// each transport phase, and nothing is retried — an upload chunk is a +// mutation at a fixed offset, so a blind replay is not safe. +type rawTransport struct { + baseURL string + apiVersion string + apiKey string + accessToken string + userProject string + apiRevision string + headers map[string]string + timeout time.Duration + client client.HTTPClient +} + +var apiVersionShape = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) + +// validAPIVersion holds the version to one inert path segment: the shape alone +// would still admit the traversal segments "." and "..". +func validAPIVersion(v string) bool { + return apiVersionShape.MatchString(v) && v != "." && v != ".." +} + +const rawUserAgent = "speakeasy-sdk/go 0.0.1 internal v1beta github.com/speakeasy-sdks/gemini-cli-next/internal/sdk (porcelain)" + +func newRawTransport(cmd *cobra.Command) (*rawTransport, error) { + t := &rawTransport{headers: map[string]string{}} + + // Server: --server-url > first declared server. + if serverURL, _ := flagutil.GetStringFlag(cmd, "server-url"); serverURL != "" { + if err := flagutil.ValidateServerURL(serverURL); err != nil { + return nil, err + } + t.baseURL = serverURL + } else { + t.baseURL = sdk.ServerList[0] + } + t.baseURL = strings.TrimRight(t.baseURL, "/") + + // API version: flag > env/config > flag default (v1beta). An explicitly + // empty flag is invalid; only an absent value may fall back to the default. + apiVersionExplicit := flagutil.FlagChanged(cmd, "api-version") + if apiVersionExplicit { + t.apiVersion, _ = flagutil.GetStringFlag(cmd, "api-version") + } else if v := config.GetString("api-version"); v != "" { + t.apiVersion = v + } else { + t.apiVersion, _ = flagutil.GetStringFlag(cmd, "api-version") + } + if t.apiVersion == "" && !apiVersionExplicit { + t.apiVersion = "v1beta" + } + // The generated client's request hook holds the version to one path + // segment; that hook does not run here, and the version is spliced into the + // upload path ("/upload/../evil/files"). + if !validAPIVersion(t.apiVersion) { + return nil, usageError("--api-version must be non-empty, contain only letters, numbers, '.', '_', or '-', and cannot be '.' or '..'") + } + + // Request credentials use the SDK client's request-scoped chain. Dry-run + // deliberately skips the OS keychain so unattended probing cannot prompt. + // Exactly one scheme is kept, ranked like the generated client + // (flag > env > keyring > config, API key on a tie). + apiKey, apiKeySource := config.ResolveRequestSecurityCredential(cmd, "api-key") + accessToken, accessTokenSource := config.ResolveRequestSecurityCredential(cmd, "access-token") + switch config.PickCredential([]config.CredentialCandidate{ + {Field: "APIKey", Complete: apiKey != "", Sources: []string{apiKeySource}}, + {Field: "AccessToken", Complete: accessToken != "", Sources: []string{accessTokenSource}}, + }, nil) { + case 0: + t.apiKey = apiKey + case 1: + t.accessToken = accessToken + } + t.userProject = resolveGlobalString(cmd, "user-project") + t.apiRevision = resolveGlobalString(cmd, "api-revision") + + // Repeatable --header "Key: Value". + if hdrs, _ := flagutil.GetStringArrayFlag(cmd, "header"); len(hdrs) > 0 { + for _, h := range hdrs { + k, v, ok := strings.Cut(h, ":") + if !ok { + return nil, fmt.Errorf("invalid header format %q: expected \"Key: Value\"", h) + } + t.headers[strings.TrimSpace(k)] = strings.TrimSpace(v) + } + } + + // Timeout: flag > env/config. + timeoutStr, changed := flagutil.GetStringFlag(cmd, "timeout") + if !changed || timeoutStr == "" { + timeoutStr = config.GetString("timeout") + } + if timeoutStr != "" { + d, err := time.ParseDuration(timeoutStr) + if err != nil { + return nil, fmt.Errorf("invalid --timeout value %q: %w", timeoutStr, err) + } + t.timeout = d + } + + // Never follow redirects: a 3xx from the upload endpoint could otherwise + // forward the resolved credentials and the file bytes to an arbitrary + // Location. The resumable protocol never legitimately redirects. + var httpClient client.HTTPClient = &http.Client{ + CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, + } + if tc := testclient.NewTestHTTPClient(); tc != nil { + httpClient = tc + } + if !isDryRun(cmd) { + // The diagnostics wrapper does not treat the resumable session id as + // sensitive. Keep it out of diagnostics and transport errors on both + // sides of the wrapper, whether or not --debug is enabled. + httpClient = sessionRedactor{client.WrapClientForDiagnostics(cmd, sessionRestorer{httpClient})} + } else { + httpClient = client.WrapClientForDiagnostics(cmd, httpClient) + } + t.client = httpClient + return t, nil +} + +// resolveGlobalString reads a global parameter: flag > env/config. +func resolveGlobalString(cmd *cobra.Command, name string) string { + if flagutil.FlagChanged(cmd, name) { + val, _ := flagutil.GetStringFlag(cmd, name) + return val + } + return config.GetString(name) +} + +// apiURL joins a path (already including the API version segment when +// needed) onto the configured server. +func (t *rawTransport) apiURL(path string) string { + return t.baseURL + "/" + strings.TrimLeft(path, "/") +} + +// newRequest builds a request against an absolute URL with the runtime's +// credentials and standard headers applied. +func (t *rawTransport) newRequest(ctx context.Context, method, absURL string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, absURL, body) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", rawUserAgent) + if t.accessToken != "" { + req.Header.Set("Authorization", "Bearer "+t.accessToken) + } else if t.apiKey != "" { + req.Header.Set("x-goog-api-key", t.apiKey) + } + if t.userProject != "" { + req.Header.Set("x-goog-user-project", t.userProject) + } + if t.apiRevision != "" { + req.Header.Set("Api-Revision", t.apiRevision) + } + for k, v := range t.headers { + req.Header.Set(k, v) + } + // Exactly one scheme leaves the process, as on generated requests: a + // caller-supplied Authorization header displaces the API key. + if req.Header.Get("Authorization") != "" { + req.Header.Del("x-goog-api-key") + } + return req, nil +} + +// do sends the request through the wrapped client, applying the configured +// timeout. Non-2xx responses are converted into the SDK's default error so +// output.Error classifies them exactly like generated commands. The timeout +// covers the response body too: it is released when the caller closes the body. +func (t *rawTransport) do(req *http.Request) (*http.Response, error) { + cancel := context.CancelFunc(func() {}) + if t.timeout > 0 { + var ctx context.Context + ctx, cancel = context.WithTimeout(req.Context(), t.timeout) + req = req.WithContext(ctx) + } + res, err := t.client.Do(req) + if err != nil { + cancel() + return nil, fmt.Errorf("error sending request: %w", err) + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + body, _ := io.ReadAll(res.Body) + res.Body.Close() + cancel() + return nil, sdkerrors.NewSDKDefaultError("API error occurred", res.StatusCode, string(body), res) + } + res.Body = cancelOnClose{res.Body, cancel} + return res, nil +} + +// cancelOnClose releases a request's timeout context once its body is closed. +type cancelOnClose struct { + io.ReadCloser + cancel context.CancelFunc +} + +func (c cancelOnClose) Close() error { + err := c.ReadCloser.Close() + c.cancel() + return err +} + +// sameService reports whether a server-provided URL (the resumable upload +// session) may receive our credentials and file bytes. It must be either the +// exact configured server (same scheme AND host — a scheme downgrade such as +// https→http on the same host is rejected) or an HTTPS Google API host. +func (t *rawTransport) sameService(raw string) bool { + u, err := url.Parse(raw) + if err != nil || !u.IsAbs() || u.Host == "" { + return false + } + if base, err := url.Parse(t.baseURL); err == nil && + strings.EqualFold(u.Host, base.Host) && strings.EqualFold(u.Scheme, base.Scheme) { + return true + } + host := strings.ToLower(u.Hostname()) + return u.Scheme == "https" && (host == "googleapis.com" || strings.HasSuffix(host, ".googleapis.com")) +} + +// The resumable upload session id ("upload_id") is a bearer-like capability: +// whoever holds the session URL can write to the upload. sessionRedactor and +// sessionRestorer sandwich the diagnostics wrapper so it only ever sees the id +// redacted — in the chunk request URL and in the start response's +// X-Goog-Upload-Url header — while the wire and the caller see the real value. +const ( + sessionQueryKey = "upload_id" + sessionURLHeader = "X-Goog-Upload-Url" +) + +type sessionSecrets struct { + requestURL *url.URL + sessionURL string +} + +type sessionSecretsKey struct{} + +// redactSessionID returns the URL with its upload_id value masked, and whether +// there was one to mask. +func redactSessionID(u *url.URL) (*url.URL, bool) { + query := u.Query() + if !query.Has(sessionQueryKey) { + return u, false + } + query.Set(sessionQueryKey, "REDACTED") + masked := *u + masked.RawQuery = query.Encode() + return &masked, true +} + +// redactSessionError masks the session id inside a transport error. Go's +// transport wraps the real request URL in *url.Error, which the diagnostics +// wrapper prints as "Transport Error" and the command reports to the user. +func redactSessionError(err error) error { + var urlErr *url.Error + if !errors.As(err, &urlErr) { + return err + } + parsed, parseErr := url.Parse(urlErr.URL) + if parseErr != nil { + return err + } + if masked, ok := redactSessionID(parsed); ok { + urlErr.URL = masked.String() + } + return err +} + +// sessionRedactor sits outside the diagnostics wrapper. +type sessionRedactor struct{ inner client.HTTPClient } + +func (c sessionRedactor) Do(req *http.Request) (*http.Response, error) { + secrets := &sessionSecrets{} + req = req.WithContext(context.WithValue(req.Context(), sessionSecretsKey{}, secrets)) + if masked, ok := redactSessionID(req.URL); ok { + secrets.requestURL = req.URL + req.URL = masked + } + res, err := c.inner.Do(req) + if res != nil && secrets.sessionURL != "" { + res.Header.Set(sessionURLHeader, secrets.sessionURL) + } + return res, err +} + +// sessionRestorer sits inside the diagnostics wrapper, next to the wire. +type sessionRestorer struct{ inner client.HTTPClient } + +func (c sessionRestorer) Do(req *http.Request) (*http.Response, error) { + secrets, _ := req.Context().Value(sessionSecretsKey{}).(*sessionSecrets) + if secrets != nil && secrets.requestURL != nil { + req = req.Clone(req.Context()) + req.URL = secrets.requestURL + } + res, err := c.inner.Do(req) + err = redactSessionError(err) + if res == nil || secrets == nil { + return res, err + } + if raw := res.Header.Get(sessionURLHeader); raw != "" { + if parsed, parseErr := url.Parse(raw); parseErr == nil { + if masked, ok := redactSessionID(parsed); ok { + secrets.sessionURL = raw + res.Header.Set(sessionURLHeader, masked.String()) + } + } + } + return res, err +} diff --git a/internal/cli/custom/register.go b/internal/cli/custom/register.go new file mode 100644 index 0000000..bff601f --- /dev/null +++ b/internal/cli/custom/register.go @@ -0,0 +1,294 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package custom hosts hand-written commands that register into the +// generated CLI. +// +// This file is generated ONCE and is never overwritten on regeneration — it +// is yours to edit. Commands added here survive regeneration, may use the +// CLI's internal packages (auth, config, output formatting, dry-run +// plumbing), and can reshape the generated command tree: add new top-level +// intent commands, claim a shared name, or mount generated groups beneath a +// curated parent command. +package custom + +import ( + "fmt" + "regexp" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Register is called after every generated command has been attached to the +// root command. It receives the fully-assembled root and may add, remove, +// wrap, or rearrange commands freely. +// +// Here it turns tier-1 commands the declarative intent layer cannot express +// into hand-written porcelain backed by the Interactions API (the CLI's own +// embedded SDK, plus a resumable uploader for the Files API): +// - tts / analyze / transcribe are declared `custom: true` in the command +// manifest; the generated intent layer attaches each one with its name, +// category, and help-group position and leaves RunE to us. +// - files upload is added under the generated files group (whose leaf +// commands — list/get/delete/register — stay generated). +// - files get / delete additionally accept a positional identifier so +// "files get files/abc" works alongside the generated "--file" flag. +// - models get accepts a positional model id ("models get gemini-2.5-flash") +// alongside the generated "--model" flag; the generated models group's +// bare invocation stays the curated catalog (merged in by the generated +// catalog layer), with list/get as its live API leaves. +func Register(root *cobra.Command) { + if err := register(root); err != nil { + // A drifted command surface (a declared custom command that no longer + // exists, a files group that lost its shape) must fail loudly at + // startup rather than silently dropping porcelain behaviour. + panic(fmt.Sprintf("custom command registration failed: %v", err)) + } +} + +func register(root *cobra.Command) error { + // tokens stays as the generated "planned" placeholder: count-tokens needs + // the classic GenAI surface that this interactions-only build drops. tts, + // analyze, and transcribe are declared custom and backed by real porcelain + // over the Interactions API. + claims := []struct { + name string + attach func(*cobra.Command) + }{ + {"tts", attachTTS}, + {"analyze", attachAnalyze}, + {"transcribe", attachTranscribe}, + } + for _, c := range claims { + cmd := findChild(root, c.name) + if cmd == nil { + return fmt.Errorf("expected custom command %q to attach to, but it is not registered", c.name) + } + c.attach(cmd) + } + + guardRequiredFlags(root) + boundStdinReads(root) + + files := findChild(root, "files") + if files == nil { + return fmt.Errorf("expected the generated files group to mount porcelain under") + } + if findChild(files, "upload") == nil { + uploadCmd := newFilesUploadCmd() + usage.MarkDynamic(uploadCmd) + files.AddCommand(uploadCmd) + } + if err := addPositionalIdentifier(findChild(files, "get"), "file", normalizeFilePositional); err != nil { + return fmt.Errorf("files get: %w", err) + } + if err := addPositionalIdentifier(findChild(files, "delete"), "file", normalizeFilePositional); err != nil { + return fmt.Errorf("files delete: %w", err) + } + + models := findChild(root, "models") + if models == nil { + return fmt.Errorf("expected the generated models group to mount porcelain under") + } + if err := addPositionalIdentifier(findChild(models, "get"), "model", normalizeModelPositional); err != nil { + return fmt.Errorf("models get: %w", err) + } + return nil +} + +// normalizeFilePositional folds "files/" or a bare id down to the bare id +// the Files API path parameter expects, and rejects empty or malformed ids +// before they reach the API: an empty segment would turn "files delete" into a +// request against the /files/ collection path. +func normalizeFilePositional(id string) (string, error) { + _, bare, ok := normalizeFileID(id) + if !ok { + return "", fmt.Errorf("invalid file id %q; expected files/ or a bare id", strings.TrimSpace(id)) + } + return bare, nil +} + +// modelIDShape is a single path segment: it forbids empty ids, slashes (path +// traversal / list-shaped requests), and other characters that would corrupt +// the /{api_version}/models/{model} path. Rejecting "/" is deliberate: it also +// excludes other resource collections such as "tunedModels/", which are +// outside the get/list-only Models surface this CLI exposes. +var modelIDShape = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) + +// normalizeModelPositional strips an optional "models/" prefix so both +// "models get gemini-2.5-flash" and "models get models/gemini-2.5-flash" +// resolve to the bare id the path parameter expects, and rejects empty or +// malformed ids before they reach the API. +func normalizeModelPositional(id string) (string, error) { + id = strings.TrimPrefix(strings.TrimSpace(id), "models/") + if !modelIDShape.MatchString(id) { + return "", fmt.Errorf("invalid model id %q; expected a model name like \"gemini-2.5-flash\"", id) + } + return id, nil +} + +// guardRequiredFlags makes every generated body-less operation fail on a +// missing or blank required flag. The generated request builder relaxes +// required flags when such a command is invoked with no flags at all, and only +// checks presence otherwise (--id "$UNSET"); either would send the request +// with an empty path segment ("DELETE /webhooks/"). +func guardRequiredFlags(parent *cobra.Command) { + for _, cmd := range parent.Commands() { + guardRequiredFlags(cmd) + original := cmd.RunE + if original == nil || cmd.Annotations["speakeasy_operation"] == "" || cmd.Flags().Lookup("body") != nil { + continue + } + cmd.RunE = func(c *cobra.Command, args []string) error { + if usageRequested(c) { + return original(c, args) + } + var missing []string + c.LocalFlags().VisitAll(func(f *pflag.Flag) { + required := len(f.Annotations[flagutil.AnnotationRequired]) > 0 + blank := f.Value.Type() == "string" && strings.TrimSpace(f.Value.String()) == "" + if required && (blank || (!f.Changed && f.DefValue == "")) { + missing = append(missing, "--"+f.Name) + } + }) + if len(missing) > 0 { + return usageError("missing required flag: " + strings.Join(missing, ", ")) + } + return original(c, args) + } + } +} + +// boundStdinReads keeps the stdin read deadline on in every mode. The +// generated pre-run enables it only in agent mode; any other caller that leaves +// a pipe open without writing to it (a test harness, a wrapper script) would +// otherwise block a body-reading command forever. An explicit "@-" still waits +// for EOF. +func boundStdinReads(root *cobra.Command) { + original := root.PersistentPreRunE + root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if original != nil { + if err := original(cmd, args); err != nil { + return err + } + } + flagutil.SetStdinReadDeadline(true) + return nil + } +} + +// findChild returns the direct child command answering to a name or alias. +func findChild(parent *cobra.Command, name string) *cobra.Command { + for _, c := range parent.Commands() { + if c.Name() == name || c.HasAlias(name) { + return c + } + } + return nil +} + +// addPositionalIdentifier lets a generated command that requires a -- +// resource identifier also accept it as a single positional argument, so +// "files get files/abc" and "files get --file files/abc" both work and both +// resolve to the same path. The normalizer runs on the identifier regardless +// of which form supplied it, and may reject a malformed id. It returns an +// error (rather than silently no-op'ing) when the command surface has drifted +// so Register fails loudly at startup. +func addPositionalIdentifier(cmd *cobra.Command, flagName string, normalize func(string) (string, error)) error { + if cmd == nil { + return fmt.Errorf("cannot add positional %q: command is not registered", flagName) + } + flag := cmd.Flags().Lookup(flagName) + if flag == nil { + return fmt.Errorf("cannot add positional to %q: flag --%s is missing", cmd.Name(), flagName) + } + original := cmd.RunE + if original == nil { + return fmt.Errorf("cannot add positional to %q: command has no RunE", cmd.Name()) + } + cmd.Use = fmt.Sprintf("%s [%s]", cmd.Name(), flagName) + // The identifier is required once, in either form. Leaving the flag itself + // marked required would make --interactive prompt for both forms and fail + // when only the argument is answered; the RunE below enforces presence. + if err := flagutil.OverridePromptRequirement(cmd, flagName, false, false); err != nil { + return err + } + delete(flag.Annotations, cobra.BashCompOneRequiredFlag) + // The generated description carries a requiredness marker, which no longer + // holds for the flag alone. + flag.Usage = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(flag.Usage), "[required]")) + if flag.Usage == "" { + flag.Usage = fmt.Sprintf("Identifier (alternative to the [%s] argument)", flagName) + } + // The positional is folded into the flag during argument validation, which + // runs before the interactive pre-run: a supplied identifier is then never + // prompted for again. + cmd.Args = func(c *cobra.Command, args []string) error { + if err := cobra.MaximumNArgs(1)(c, args); err != nil { + return err + } + if len(args) == 0 { + return nil + } + if flagutil.FlagChanged(c, flagName) { + return usageError(fmt.Sprintf("pass the identifier once: as an argument or via --%s, not both", flagName)) + } + return c.Flags().Set(flagName, args[0]) + } + // The generation-time --usage schema knows only the flag; render it live so + // the positional form is advertised too. + declareInteractive(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{{ + Name: flagName, Summary: "Identifier (or use --" + flagName + ")", Required: true, SatisfiedBy: []string{flagName}, + }}}) + usage.MarkDynamic(cmd) + cmd.RunE = func(c *cobra.Command, args []string) error { + // An identifier answered at the interactive prompt arrives here as an + // argument: argument validation ran before the prompt. + if len(args) == 1 && !flagutil.FlagChanged(c, flagName) { + if err := c.Flags().Set(flagName, args[0]); err != nil { + return err + } + } + if !flagutil.FlagChanged(c, flagName) { + // The generated request builder relaxes required flags when a + // body-less command is invoked with no flags at all, which would + // send the request with an empty path segment. + return usageError(fmt.Sprintf("missing %s identifier: pass it as an argument or via --%s", flagName, flagName)) + } + // Normalize/validate whatever now populates the flag — positional or + // -- — so the path parameter shape is identical either way. + if normalize != nil { + raw, err := c.Flags().GetString(flagName) + if err != nil { + return err + } + norm, err := normalize(raw) + if err != nil { + return usageError(err.Error()) + } + if norm != raw { + if err := c.Flags().Set(flagName, norm); err != nil { + return err + } + } + } + return original(c, nil) + } + return nil +} diff --git a/internal/cli/custom/transcribe.go b/internal/cli/custom/transcribe.go new file mode 100644 index 0000000..1e07c6a --- /dev/null +++ b/internal/cli/custom/transcribe.go @@ -0,0 +1,428 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/spf13/cobra" +) + +// transcribeFormats maps the --format values to the artifact extension. +var transcribeFormats = map[string]string{"md": ".md", "text": ".txt", "json": ".json", "srt": ".srt"} + +// attachTranscribe turns the (claimed) transcribe command into the +// speech-to-text porcelain: a model interaction over an audio/video content +// block with a transcription prompt (schema-enforced structured output for +// json/srt), the transcript written to disk, absolute path on stdout. +func attachTranscribe(cmd *cobra.Command) { + cmd.Use = "transcribe" + cmd.Long = "Transcribe one or more audio or video inputs.\n\nPass each local path or uploaded files/ with --input. Inputs are validated\nbefore transcription, then processed sequentially. By default, stdout prints one\nabsolute artifact path per line. Formats: md (default), text, json, and srt\n(alias: captions). With multiple inputs, --out names a directory rather than a\nfile; an existing artifact is replaced. Requests are not stored server-side.\n\n" + inlineLimitNote + cmd.Example = " gemini-api transcribe -i interview.mp3\n" + + " gemini-api transcribe -i call.wav --format srt --out call.srt\n" + + " gemini-api transcribe -i files/abc123 --format json --no-speakers\n" + + " gemini-api transcribe -i a.mp3 -i b.mp3 --out ./transcripts/" + helpMeta(cmd, "model "+defaultTranscribeModel+" · format md · speakers on · timestamps on", + "https://ai.google.dev/gemini-api/docs/audio", + "full request control via gemini-api agent run") + cmd.Args = cobra.NoArgs + cmd.Flags().StringArrayP("input", "i", nil, "Local path or files/ to transcribe (repeatable)") + cmd.Flags().String("format", "md", "Transcript format: md, text, json, srt (alias: captions)") + cmd.Flags().Bool("no-speakers", false, "Do not label speakers") + cmd.Flags().Bool("no-timestamps", false, "Do not include timestamps (ignored for srt)") + cmd.Flags().String("out", "", "Output path (default: ./transcript--.)") + cmd.Flags().String("mime-type", "", "Override the detected MIME type (one input only)") + modelFlag(cmd, defaultTranscribeModel, "transcription") + annotatePromptFlag(cmd, "input", flagutil.PromptFlagSpec{Required: true, Kind: "string-array", Order: 0}) + cmd.RunE = runTranscribe +} + +func runTranscribe(cmd *cobra.Command, args []string) error { + if usageRequested(cmd) { + return emitUsageKDL(cmd, cmd.OutOrStdout()) + } + inputs, _ := cmd.Flags().GetStringArray("input") + if len(inputs) == 0 { + return output.UsageHelpError(cmd, errors.New("missing required flag --input (a local audio/video path or files/)")) + } + format, _ := flagutil.GetStringFlag(cmd, "format") + format = strings.ToLower(strings.TrimSpace(format)) + if format == "captions" { + format = "srt" + } + ext, ok := transcribeFormats[format] + if !ok { + return usageError(fmt.Sprintf("invalid --format %q", format), "Valid formats: md, text, json, srt") + } + noSpeakers, _ := flagutil.GetBoolFlag(cmd, "no-speakers") + noTimestamps, _ := flagutil.GetBoolFlag(cmd, "no-timestamps") + speakers, timestamps := !noSpeakers, !noTimestamps + if format == "srt" { + timestamps = true + } + structured := format == "json" || format == "srt" + model, err := resolveModel(cmd, defaultTranscribeModel) + if err != nil { + return err + } + s, sources, err := resolveMediaSources(cmd, inputs, transcribePolicy) + if err != nil { + return err + } + paths, err := transcribeArtifactPaths(cmd, sources, ext) + if err != nil { + return err + } + + opts, err := callOpts(cmd) + if err != nil { + return err + } + + results := make([]map[string]any, 0, len(sources)) + // fail names the artifacts already written, which the final result would + // otherwise have carried, so a later failure does not orphan them. They + // travel as hints so every error rendering (human, JSON envelope, agent + // mode) carries them and stderr stays a single document. + fail := func(err error) error { + if len(results) == 0 { + return err + } + hints := make([]string, 0, len(results)) + for _, done := range results { + hints = append(hints, fmt.Sprintf("Completed before the failure: %s", done["path"])) + } + var hinted interface{ CLIHints() []string } + if errors.As(err, &hinted) { + hints = append(hints, hinted.CLIHints()...) + } + return porcelainError{error: err, hints: hints} + } + for i, src := range sources { + media, err := src.block(cmd) + if err != nil { + return fail(err) + } + body := newModelInteraction(model, media, textContentBlock(transcribePrompt(format, speakers, timestamps))) + if structured { + // Schema-enforced JSON; parseTranscript also tolerates fenced output. + format := interactions.CreateCreateModelInteractionResponseFormatResponseFormat( + interactions.CreateResponseFormatTextResponseFormat(interactions.TextResponseFormat{ + MimeType: interactions.TextResponseFormatMimeTypeApplicationJSON.ToPointer(), + Schema: transcriptSchema(speakers, timestamps), + })) + body.ResponseFormat = &format + } + req := operations.CreateInteractionRequest{ + Body: operations.CreateCreateInteractionRequestBodyCreateModelInteraction(body), + } + if isDryRun(cmd) { + if _, err := s.Agent.Run(cmd.Context(), req, opts...); err != nil { + return err + } + continue + } + + progress(cmd, "Transcribing %s with %s...", src.label, model) + res, err := s.Agent.Run(cmd.Context(), req, opts...) + if err != nil { + progress(cmd, "%s: request failed", inputRef(i+1, inputs[i])) + return output.Error(cmd, fail(err)) + } + text, err := interactionText(res.Interaction) + if err != nil { + return fail(err) + } + artifact, err := formatTranscriptArtifact(cmd, text, format, speakers, timestamps) + if err != nil { + return fail(err) + } + if err := writeArtifact(paths[i], []byte(artifact)); err != nil { + return fail(runtimeError(fmt.Sprintf("cannot write %s: %v", paths[i], err))) + } + progress(cmd, "Wrote %s (%d bytes).", paths[i], len(artifact)) + result := map[string]any{ + "input": src.label, "path": paths[i], "size_bytes": len(artifact), + } + if src.mimeType != "" { + result["mime_type"] = src.mimeType + } + if u := usageEnvelope(res.Interaction.Usage); u != nil { + result["usage"] = u + } + results = append(results, result) + } + if isDryRun(cmd) { + return nil + } + return emitResult(cmd, strings.Join(paths, "\n"), map[string]any{ + "model": model, "format": format, "results": results, + }) +} + +// formatTranscriptArtifact renders the model's answer in the requested format. +// Only srt checks the timestamps' ranges and order: a caption file with broken +// timing misleads, while json hands the raw segments over as the API gave them, +// which is also the way out when srt rejects a transcript. +func formatTranscriptArtifact(cmd *cobra.Command, text, format string, speakers, timestamps bool) (string, error) { + switch format { + case "json": + segments, err := parseTranscript(text, speakers, timestamps) + if err != nil { + return "", err + } + pretty, _ := json.MarshalIndent(map[string]any{"segments": segments}, "", " ") + return string(pretty) + "\n", nil + case "srt": + segments, err := parseTranscript(text, speakers, true) + if err != nil { + return "", err + } + artifact, err := renderSRT(segments, speakers) + if err != nil { + return "", runtimeError("cannot render SRT captions: "+err.Error(), + "Re-run with --format json to keep the raw segments") + } + return artifact, nil + default: + return strings.TrimRight(text, "\n") + "\n", nil + } +} + +var unsafeArtifactName = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) + +func transcribeArtifactPaths(cmd *cobra.Command, sources []*mediaSource, ext string) ([]string, error) { + out, _ := flagutil.GetStringFlag(cmd, "out") + out = strings.TrimSpace(out) + paths := make([]string, len(sources)) + // A single input may still name a directory, which is never treated as the + // artifact file itself. + if out == "" || (len(sources) == 1 && !namesDirectory(out)) { + for i := range sources { + path, err := artifactPath(cmd, "out", "transcript", ext) + if err != nil { + return nil, err + } + paths[i] = path + } + return paths, nil + } + info, err := os.Stat(out) + switch { + case err == nil && !info.IsDir(): + return nil, usageError(fmt.Sprintf("--out %q must be a directory when transcribing multiple inputs", out)) + case err != nil && !os.IsNotExist(err): + return nil, usageError(fmt.Sprintf("cannot inspect --out %q: %v", out, err)) + case os.IsNotExist(err) && isDryRun(cmd): + // Dry-run previews the requests without touching the filesystem; + // the paths are still computed so the preview names them. + case os.IsNotExist(err): + if err := os.MkdirAll(out, 0o755); err != nil { + return nil, usageError(fmt.Sprintf("cannot create --out directory %q: %v", out, err)) + } + } + absDir, err := filepath.Abs(out) + if err != nil { + return nil, usageError(fmt.Sprintf("cannot resolve --out %q: %v", out, err)) + } + for i, src := range sources { + base := filepath.Base(src.label) + base = strings.TrimSuffix(base, filepath.Ext(base)) + base = strings.Trim(unsafeArtifactName.ReplaceAllString(base, "-"), ".-_") + if base == "" { + base = "input" + } + paths[i] = filepath.Join(absDir, fmt.Sprintf("%s-%d%s", base, i+1, ext)) + } + return paths, nil +} + +// transcribePrompt builds the instruction for the requested shape. +func transcribePrompt(format string, speakers, timestamps bool) string { + parts := []string{"Transcribe the speech in this recording accurately and completely."} + if speakers { + parts = append(parts, "Identify and label distinct speakers (Speaker 1, Speaker 2, ...).") + } else { + parts = append(parts, "Do not include speaker labels.") + } + if timestamps { + parts = append(parts, "Include accurate timestamps (mm:ss or hh:mm:ss) for each segment.") + } else { + parts = append(parts, "Do not include timestamps.") + } + switch format { + case "md": + parts = append(parts, "Format the output as clean Markdown.") + case "text": + parts = append(parts, "Format the output as plain text.") + case "json", "srt": + parts = append(parts, "Return JSON matching the response schema: one segment per utterance.") + } + return strings.Join(parts, " ") +} + +// transcriptSchema is the structured-output JSON schema for json/srt +// transcripts; its keys are the ones parseTranscript requires. +func transcriptSchema(speakers, timestamps bool) map[string]any { + props := map[string]any{ + "content": map[string]any{"type": "string", "description": "Transcribed text of the segment"}, + } + required := []string{"content"} + if speakers { + props["speaker"] = map[string]any{"type": "string", "description": "Speaker label, e.g. Speaker 1"} + required = append(required, "speaker") + } + if timestamps { + props["start_time"] = map[string]any{"type": "string", "description": "Segment start, mm:ss or hh:mm:ss(.mmm)"} + props["end_time"] = map[string]any{"type": "string", "description": "Segment end, mm:ss or hh:mm:ss(.mmm)"} + required = append(required, "start_time", "end_time") + } + return map[string]any{ + "type": "object", + "properties": map[string]any{ + "segments": map[string]any{ + "type": "array", + "description": "Transcript segments in order", + "items": map[string]any{"type": "object", "properties": props, "required": required}, + }, + }, + "required": []string{"segments"}, + } +} + +// transcriptSegment is one structured transcript entry. +type transcriptSegment struct { + Speaker string `json:"speaker,omitempty"` + StartTime string `json:"start_time,omitempty"` + EndTime string `json:"end_time,omitempty"` + Content string `json:"content"` +} + +// parseTranscript validates the model's structured JSON transcript. +func parseTranscript(raw string, speakers, timestamps bool) ([]transcriptSegment, error) { + var payload struct { + Segments []transcriptSegment `json:"segments"` + } + raw = strings.TrimSpace(raw) + raw = strings.TrimPrefix(raw, "```json") + raw = strings.TrimPrefix(raw, "```") + raw = strings.TrimSuffix(raw, "```") + if err := json.Unmarshal([]byte(raw), &payload); err != nil { + return nil, runtimeError(fmt.Sprintf("transcription response is not valid JSON: %v", err)) + } + if len(payload.Segments) == 0 { + return nil, runtimeError("transcription response has no segments") + } + for i, seg := range payload.Segments { + if strings.TrimSpace(seg.Content) == "" { + return nil, runtimeError(fmt.Sprintf("segment %d has no content", i+1)) + } + if timestamps && (seg.StartTime == "" || seg.EndTime == "") { + return nil, runtimeError(fmt.Sprintf("segment %d is missing start_time/end_time", i+1)) + } + if speakers && seg.Speaker == "" { + payload.Segments[i].Speaker = "Speaker" + } + } + return payload.Segments, nil +} + +// renderSRT renders segments as SubRip captions. A segment that ends before it +// starts, or starts before its predecessor, is an error rather than a silent +// repair; overlapping segments (crosstalk) are fine. +func renderSRT(segments []transcriptSegment, speakers bool) (string, error) { + var sb strings.Builder + var prevStart int64 + for i, seg := range segments { + start, err := parseTimestamp(seg.StartTime) + if err != nil { + return "", fmt.Errorf("segment %d: bad start_time %q", i+1, seg.StartTime) + } + end, err := parseTimestamp(seg.EndTime) + if err != nil { + return "", fmt.Errorf("segment %d: bad end_time %q", i+1, seg.EndTime) + } + if end < start { + return "", fmt.Errorf("segment %d: end_time %q is before start_time %q", i+1, seg.EndTime, seg.StartTime) + } + if start < prevStart { + return "", fmt.Errorf("segment %d: start_time %q is before the previous segment's", i+1, seg.StartTime) + } + prevStart = start + // A blank line ends an SRT cue, so none may survive inside the text. + line := blankLines.ReplaceAllString(strings.TrimSpace(seg.Content), "\n") + if speakers && seg.Speaker != "" { + line = seg.Speaker + ": " + line + } + fmt.Fprintf(&sb, "%d\n%s --> %s\n%s\n\n", i+1, formatSRTTime(start), formatSRTTime(end), line) + } + return sb.String(), nil +} + +var ( + blankLines = regexp.MustCompile(`\n\s*\n`) + timestampWhole = regexp.MustCompile(`^\d+$`) + timestampSeconds = regexp.MustCompile(`^\d+(\.\d+)?$`) +) + +// parseTimestamp accepts "ss", "ss.mmm", "mm:ss", "mm:ss.mmm", "hh:mm:ss[.mmm]" +// (also with a comma decimal separator) and returns milliseconds. Only the +// last field may carry a fraction, and every field after the first must be +// below 60; the first is unbounded, so "125.5" and "90:15" are valid. +func parseTimestamp(s string) (int64, error) { + s = strings.TrimSpace(strings.ReplaceAll(s, ",", ".")) + if s == "" { + return 0, fmt.Errorf("empty timestamp") + } + fields := strings.Split(s, ":") + if len(fields) > 3 { + return 0, fmt.Errorf("too many fields") + } + var total float64 + for i, f := range fields { + shape := timestampWhole + if i == len(fields)-1 { + shape = timestampSeconds + } + v, err := strconv.ParseFloat(f, 64) + if err != nil || !shape.MatchString(f) { + return 0, fmt.Errorf("invalid field %q", f) + } + if i > 0 && v >= 60 { + return 0, fmt.Errorf("field %q is out of range", f) + } + total = total*60 + v + } + return int64(total*1000 + 0.5), nil +} + +func formatSRTTime(ms int64) string { + h := ms / 3600000 + m := (ms % 3600000) / 60000 + sec := (ms % 60000) / 1000 + milli := ms % 1000 + return fmt.Sprintf("%02d:%02d:%02d,%03d", h, m, sec, milli) +} diff --git a/internal/cli/custom/tts.go b/internal/cli/custom/tts.go new file mode 100644 index 0000000..50cd2e8 --- /dev/null +++ b/internal/cli/custom/tts.go @@ -0,0 +1,303 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "encoding/base64" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/spf13/cobra" +) + +// attachTTS turns the (claimed) tts command into the text-to-speech porcelain: +// a model interaction requesting the audio response format with a speech +// config, the returned audio written to disk (raw PCM wrapped as WAV), the +// absolute path on stdout. +func attachTTS(cmd *cobra.Command) { + cmd.Use = "tts [text]" + cmd.Long = "Convert text to speech.\n\nGenerated audio is written to a local file as WAV (24 kHz mono). The extension\nfollows the audio: --out foo.mp3 is written as foo.wav, and --out naming a\ndirectory places a default-named file inside it. An existing file is replaced.\nBy default, stdout prints the absolute artifact path. The request is not stored\nserver-side.\n\nArguments:\n Text to speak (or use -f / --stdin)" + cmd.Example = " gemini-api tts \"Welcome to the show\" # → ./gemini-tts--.wav\n" + + " gemini-api tts -f script.txt --voice Puck --out out.wav\n" + + " echo \"hello\" | gemini-api tts --stdin\n" + + " gemini-api tts \"Alice: hi. Bob: hello.\" --multi-speaker \"Alice=Kore,Bob=Puck\"" + helpMeta(cmd, "model "+defaultTTSModel+" · voice "+defaultTTSVoice+" · 24kHz mono WAV", + "https://ai.google.dev/gemini-api/docs/speech-generation", + "full request control via gemini-api agent run") + cmd.Args = cobra.ArbitraryArgs + cmd.Flags().StringP("voice", "v", defaultTTSVoice, "Voice for single-speaker audio (e.g. Kore, Puck, Zephyr, Charon, Fenrir, Aoede)") + cmd.Flags().String("multi-speaker", "", "Speaker→voice map for two-speaker scripts, e.g. \"Alice=Kore,Bob=Puck\"") + cmd.Flags().StringP("file", "f", "", "Read the input text from a file") + cmd.Flags().Bool("stdin", false, "Read the input text from stdin") + cmd.Flags().String("out", "", "Output file or directory; the extension follows the audio (default: ./gemini-tts--.wav)") + cmd.Flags().String("language", "", "BCP-47 language code hint for the speech (e.g. en-US)") + modelFlag(cmd, defaultTTSModel, "tts") + declareInteractive(cmd, interactive.CommandSpec{Args: []interactive.ArgSpec{{ + Name: "text", Summary: "Text to speak", Required: true, Variadic: true, + SatisfiedBy: []string{"file", "stdin"}, + }}}) + cmd.RunE = runTTS +} + +func runTTS(cmd *cobra.Command, args []string) error { + if usageRequested(cmd) { + return emitUsageKDL(cmd, cmd.OutOrStdout()) + } + text, err := textInput(cmd, args, "file", "stdin") + if err != nil { + return err + } + if text == "" { + return output.UsageHelpError(cmd, errors.New("missing required input text (pass it as an argument, --file, or --stdin)")) + } + + model, err := resolveModel(cmd, defaultTTSModel) + if err != nil { + return err + } + speech, voiceLabel, err := buildSpeechConfig(cmd) + if err != nil { + return err + } + // The format stays bare: the TTS models reject every explicit mime_type + // and delivery ("not supported for models/…"), and answer with 24 kHz L16. + responseFormat := interactions.CreateCreateModelInteractionResponseFormatResponseFormat( + interactions.CreateResponseFormatAudioResponseFormat(interactions.AudioResponseFormat{})) + // Warnings reach a human on stderr and a machine in the result envelope; + // structured stderr stays reserved for the error document. + var warnings []string + if speech.SpeakerConfig != nil { + // The API does not check names against the script; a stray one is + // silently voiced wrong. + for _, sp := range speech.SpeakerConfig.Speakers { + if !containsWord(text, *sp.Speaker) { + warnings = append(warnings, fmt.Sprintf("speaker %q does not appear in the text", *sp.Speaker)) + } + } + } + for _, warning := range warnings { + progress(cmd, "warning: %s", warning) + } + body := newModelInteraction(model, textContentBlock(text)) + body.ResponseFormat = &responseFormat + body.GenerationConfig = &interactions.GenerationConfig{SpeechConfig: speech} + req := operations.CreateInteractionRequest{ + Body: operations.CreateCreateInteractionRequestBodyCreateModelInteraction(body), + } + + s, err := client.NewClient(cmd) + if err != nil { + return err + } + opts, err := callOpts(cmd) + if err != nil { + return err + } + if isDryRun(cmd) { + _, err := s.Agent.Run(cmd.Context(), req, opts...) + return err + } + + progress(cmd, "Synthesizing speech with %s (voice %s)...", model, voiceLabel) + res, err := s.Agent.Run(cmd.Context(), req, opts...) + if err != nil { + return output.Error(cmd, err) + } + audio, mimeType, channels, sampleRate, err := extractAudio(res.Interaction) + if err != nil { + return err + } + + // Prefer the channel/rate the API reported on the audio block; fall back to + // the MIME parameters, then to the 24 kHz mono the TTS models emit. + format := parseAudioMIME(mimeType) + if channels == 0 { + channels = format.channels + } + if sampleRate == 0 { + sampleRate = format.sampleRate + } + data := audio + if format.isPCM { + if channels == 0 { + channels = 1 + } + if sampleRate == 0 { + sampleRate = 24000 + } + data = append(wavHeader(len(audio), sampleRate, channels), audio...) + } + ext, ok := extensionForAudioMIME(mimeType) + if !ok { + return runtimeError(fmt.Sprintf("the API returned audio as %s, which cannot be written as a playable file", mimeType)) + } + path, err := artifactPath(cmd, "out", "gemini-tts", ext) + if err != nil { + return err + } + if err := writeArtifact(path, data); err != nil { + return runtimeError(fmt.Sprintf("cannot write %s: %v", path, err)) + } + progress(cmd, "Wrote %s (%d bytes).", path, len(data)) + + envelope := map[string]any{ + "model": model, + "path": path, + "mime_type": mimeType, + "size_bytes": len(data), + } + if format.isPCM { + envelope["sample_rate"] = sampleRate + envelope["channels"] = channels + } + if len(warnings) > 0 { + envelope["warnings"] = warnings + } + if u := usageEnvelope(res.Interaction.Usage); u != nil { + envelope["usage"] = u + } + return emitResult(cmd, path, envelope) +} + +// containsWord reports whether text names word on its own rather than inside a +// longer one ("Ann" is absent from "Annual"). +func containsWord(text, word string) bool { + edge := `[^\p{L}\p{N}]` + return regexp.MustCompile(`(^|` + edge + `)` + regexp.QuoteMeta(word) + `($|` + edge + `)`).MatchString(text) +} + +// buildSpeechConfig turns --voice / --multi-speaker / --language into the +// interaction's speech configuration: a single unnamed voice (array-of-one) or, +// for --multi-speaker, a per-speaker map. The returned label names the voice +// selection for the progress line. +func buildSpeechConfig(cmd *cobra.Command) (*interactions.SpeechConfigUnion, string, error) { + var lang *string + if l, _ := flagutil.GetStringFlag(cmd, "language"); strings.TrimSpace(l) != "" { + lang = stringPtr(strings.TrimSpace(l)) + } + spec, _ := flagutil.GetStringFlag(cmd, "multi-speaker") + if strings.TrimSpace(spec) == "" { + voice, _ := flagutil.GetStringFlag(cmd, "voice") + voice = strings.TrimSpace(voice) + if voice == "" { + voice = defaultTTSVoice + } + u := interactions.CreateSpeechConfigUnionArrayOfSpeechConfig([]interactions.SpeechConfig{{ + Voice: stringPtr(voice), Language: lang, + }}) + return &u, voice, nil + } + if flagutil.FlagChanged(cmd, "voice") { + return nil, "", usageError("--voice and --multi-speaker are mutually exclusive") + } + var speakers []interactions.SpeechConfig + seen := map[string]bool{} + for _, pair := range strings.Split(spec, ",") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + name, voice, ok := strings.Cut(pair, "=") + // Names stay case-sensitive: each must match its label in the script. + name, voice = strings.Join(strings.Fields(name), " "), strings.TrimSpace(voice) + if !ok || name == "" || voice == "" { + return nil, "", usageError(fmt.Sprintf("invalid --multi-speaker entry %q: expected Speaker=Voice", pair)) + } + if seen[name] { + return nil, "", usageError(fmt.Sprintf("--multi-speaker names speaker %q more than once", name)) + } + seen[name] = true + speakers = append(speakers, interactions.SpeechConfig{ + Speaker: stringPtr(name), Voice: stringPtr(voice), Language: lang, + }) + } + // The API takes exactly two speakers and rejects any other count opaquely. + if len(speakers) != 2 { + msg := fmt.Sprintf("--multi-speaker needs exactly two Speaker=Voice entries (got %d)", len(speakers)) + if len(speakers) == 1 { + return nil, "", usageError(msg, "Use --voice for a single speaker") + } + return nil, "", usageError(msg) + } + u := interactions.CreateSpeechConfigUnionSpeakerConfig(interactions.SpeakerConfig{Speakers: speakers}) + return &u, "multi-speaker " + strings.TrimSpace(spec), nil +} + +// extractAudio concatenates the inline audio blocks of the completed +// interaction's model-output steps and returns the decoded bytes plus the +// reported MIME type, channel count, and sample rate (0 when the API omits +// them). Raw PCM (audio/l16, …) is the common case for the TTS models. Blocks +// that disagree on format cannot be joined into one playable file. +func extractAudio(it *interactions.Interaction) ([]byte, string, int, int, error) { + if err := interactionOutcome(it); err != nil { + return nil, "", 0, 0, err + } + var audio []byte + mimeType := "" + channels, sampleRate := 0, 0 + for _, step := range it.Steps { + if step.ModelOutputStep == nil { + continue + } + for _, block := range step.ModelOutputStep.Content { + ac := block.AudioContent + if ac == nil || ac.Data == nil { + continue + } + chunk, err := base64.StdEncoding.DecodeString(*ac.Data) + if err != nil { + return nil, "", 0, 0, runtimeError(fmt.Sprintf("audio payload is not valid base64: %v", err)) + } + blockMIME, blockChannels, blockRate := "", 0, 0 + if ac.MimeType != nil { + blockMIME = string(*ac.MimeType) + } + if ac.Channels != nil { + blockChannels = *ac.Channels + } + if ac.SampleRate != nil { + blockRate = *ac.SampleRate + } + if len(audio) == 0 { + mimeType, channels, sampleRate = blockMIME, blockChannels, blockRate + } else if blockMIME != mimeType || blockChannels != channels || blockRate != sampleRate { + return nil, "", 0, 0, runtimeError("the API returned audio blocks in different formats, which cannot be joined into one file") + } + audio = append(audio, chunk...) + } + } + if len(audio) == 0 { + if msg := firstInteractionError(it); msg != "" { + return nil, "", 0, 0, runtimeError("the API returned no audio: "+msg, + "Check that the model supports speech output (default: "+defaultTTSModel+")") + } + return nil, "", 0, 0, runtimeError("no audio data in the API response", + "Check that the model supports speech output (default: "+defaultTTSModel+")") + } + if mimeType == "" { + // The TTS models return raw 16-bit PCM; assume L16 when the block omits + // its MIME so the bytes are still wrapped as a playable WAV. + mimeType = "audio/l16" + } + return audio, mimeType, channels, sampleRate, nil +} diff --git a/internal/cli/custom/wav.go b/internal/cli/custom/wav.go new file mode 100644 index 0000000..7ff59d0 --- /dev/null +++ b/internal/cli/custom/wav.go @@ -0,0 +1,113 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "encoding/binary" + "strconv" + "strings" +) + +// audioFormat is the parsed shape of an audio MIME type such as +// "audio/L16;codec=pcm;rate=24000". +type audioFormat struct { + baseMIME string + sampleRate int + channels int + isPCM bool +} + +// parseAudioMIME extracts the base type and PCM parameters from an audio MIME +// type. PCM channel count defaults to 1 (mono) when absent, matching what the +// TTS models return. +func parseAudioMIME(mimeType string) audioFormat { + parts := strings.Split(mimeType, ";") + f := audioFormat{baseMIME: strings.ToLower(strings.TrimSpace(parts[0]))} + for _, p := range parts[1:] { + key, val, ok := strings.Cut(strings.TrimSpace(p), "=") + if !ok { + continue + } + n, err := strconv.Atoi(strings.TrimSpace(val)) + if err != nil || n <= 0 { + continue + } + switch strings.ToLower(strings.TrimSpace(key)) { + case "rate", "samplerate", "sample_rate": + f.sampleRate = n + case "channels", "channelcount", "channel_count": + f.channels = n + } + } + switch f.baseMIME { + case "audio/l16", "audio/pcm", "audio/raw": + f.isPCM = true + if f.channels == 0 { + f.channels = 1 + } + } + return f +} + +// wavHeader builds a canonical 44-byte RIFF/WAVE header for 16-bit PCM. +func wavHeader(pcmLen, sampleRate, channels int) []byte { + const bitsPerSample = 16 + blockAlign := channels * bitsPerSample / 8 + byteRate := sampleRate * blockAlign + h := make([]byte, 44) + copy(h[0:], "RIFF") + binary.LittleEndian.PutUint32(h[4:], uint32(36+pcmLen)) + copy(h[8:], "WAVE") + copy(h[12:], "fmt ") + binary.LittleEndian.PutUint32(h[16:], 16) + binary.LittleEndian.PutUint16(h[20:], 1) + binary.LittleEndian.PutUint16(h[22:], uint16(channels)) + binary.LittleEndian.PutUint32(h[24:], uint32(sampleRate)) + binary.LittleEndian.PutUint32(h[28:], uint32(byteRate)) + binary.LittleEndian.PutUint16(h[32:], uint16(blockAlign)) + binary.LittleEndian.PutUint16(h[34:], bitsPerSample) + copy(h[36:], "data") + binary.LittleEndian.PutUint32(h[40:], uint32(pcmLen)) + return h +} + +// extensionForAudioMIME picks the artifact extension for a returned audio +// MIME type. Raw PCM is wrapped as WAV, so it maps to .wav. ok is false for a +// type the CLI cannot write as a playable file — headerless companded audio +// (audio/alaw, audio/mulaw) or anything unknown — rather than guessing .wav for +// bytes that are not a WAV. +func extensionForAudioMIME(mimeType string) (ext string, ok bool) { + format := parseAudioMIME(mimeType) + if format.isPCM { + return ".wav", true + } + switch format.baseMIME { + case "audio/wav", "audio/x-wav", "audio/wave": + return ".wav", true + case "audio/mp3", "audio/mpeg": + return ".mp3", true + case "audio/aac": + return ".aac", true + case "audio/ogg", "audio/ogg_opus", "audio/vorbis": + return ".ogg", true + case "audio/flac": + return ".flac", true + case "audio/opus": + return ".opus", true + case "audio/m4a", "audio/mp4": + return ".m4a", true + } + return "", false +} diff --git a/internal/cli/environments/createenvironment.go b/internal/cli/environments/createenvironment.go new file mode 100644 index 0000000..aee5f40 --- /dev/null +++ b/internal/cli/environments/createenvironment.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var createEnvironmentCmdMeta = []flagutil.FlagMeta{ + {FlagName: "from-environment", Shorthand: "f", FieldPath: "Body.FromEnvironment", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The source environment to copy/fork from.\nFormat: `environments/{environment_id}` or `{environment_id}`.\nWhen specified, `sources` and `env` must be empty."}, + {FlagName: "network", Shorthand: "n", FieldPath: "Body.Network", Kind: flagutil.FlagKindUnion, Union: &flagutil.UnionMeta{Discriminated: false, Optional: true, TypeDescription: "JSON value (one of: { \"allowlist\": object[] } | Disabled | CreateEnvironmentRequest_network_enum)"}}, + {FlagName: "sources", Shorthand: "s", FieldPath: "Body.Sources", Kind: flagutil.FlagKindJSON, Optional: true, Annotations: `json:"sources,omitempty"`, Description: "Sources to be mounted into the environment."}, +} + +// initCreateEnvironmentCmd initializes the create-environment command. +func initCreateEnvironmentCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "create", + Short: "Creates an environment.", + Long: "Creates an environment.", + Example: " gemini-api environments create", + Args: cobra.NoArgs, + RunE: runCreateEnvironmentCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateEnvironment", + }, + } + flagutil.RegisterFlags(cmd, createEnvironmentCmdMeta) + if err := flagutil.ValidateMeta[operations.CreateEnvironmentRequest](createEnvironmentCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for create-environment: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, createEnvironmentCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for create-environment: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runCreateEnvironmentCmd executes the create-environment command. +func runCreateEnvironmentCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateEnvironment") + } + req, err := flagutil.BuildRequest[operations.CreateEnvironmentRequest](cmd, createEnvironmentCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Environments.CreateEnvironment(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/environments/deleteenvironment.go b/internal/cli/environments/deleteenvironment.go new file mode 100644 index 0000000..ac737d3 --- /dev/null +++ b/internal/cli/environments/deleteenvironment.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var deleteEnvironmentCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]"}, +} + +// initDeleteEnvironmentCmd initializes the delete-environment command. +func initDeleteEnvironmentCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete", + Short: "Deletes an environment.", + Long: "Deletes an environment.", + Example: " gemini-api environments delete --id ", + Args: cobra.NoArgs, + RunE: runDeleteEnvironmentCmd, + Annotations: map[string]string{ + "speakeasy_operation": "DeleteEnvironment", + }, + } + flagutil.RegisterFlags(cmd, deleteEnvironmentCmdMeta) + if err := flagutil.ValidateMeta[operations.DeleteEnvironmentRequest](deleteEnvironmentCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for delete-environment: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runDeleteEnvironmentCmd executes the delete-environment command. +func runDeleteEnvironmentCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.DeleteEnvironmentRequest](cmd, deleteEnvironmentCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Environments.DeleteEnvironment(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/environments/environmentsfiles/list.go b/internal/cli/environments/environmentsfiles/list.go new file mode 100644 index 0000000..4027484 --- /dev/null +++ b/internal/cli/environments/environmentsfiles/list.go @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environmentsfiles + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listCmdMeta = []flagutil.FlagMeta{ + {FlagName: "environment", Shorthand: "e", FieldPath: "Environment", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, + {FlagName: "path", FieldPath: "Path", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. Maximum number of entries to return per page (for directory listing)."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Pagination token for directory listing."}, + {FlagName: "recursive", Shorthand: "r", FieldPath: "Recursive", Kind: flagutil.FlagKindBool, Optional: true, Description: "Optional. If true and the path is a directory, recursively lists all files."}, +} + +// initListCmd initializes the list command. +func initListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper.", + Long: "Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper.", + Example: " gemini-api files list --environment --path /var/mail", + Args: cobra.NoArgs, + RunE: runListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "GetEnvironmentFiles", + }, + } + flagutil.RegisterFlags(cmd, listCmdMeta) + if err := flagutil.ValidateMeta[operations.GetEnvironmentFilesRequest](listCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runListCmd executes the list command. +func runListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetEnvironmentFilesRequest](cmd, listCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Environments.Files.List(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/environments/environmentsfiles/root.go b/internal/cli/environments/environmentsfiles/root.go new file mode 100644 index 0000000..5d1c400 --- /dev/null +++ b/internal/cli/environments/environmentsfiles/root.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environmentsfiles + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitEnvironmentsFilesRoot(parent *cobra.Command) error { + var EnvironmentsFilesCmd = &cobra.Command{ + Use: "files", + Short: "Operations for files", + Long: "Operations for files", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initListCmd(EnvironmentsFilesCmd); err != nil { + return err + } + + parent.AddCommand(EnvironmentsFilesCmd) + return nil +} diff --git a/internal/cli/environments/getenvironment.go b/internal/cli/environments/getenvironment.go new file mode 100644 index 0000000..625f912 --- /dev/null +++ b/internal/cli/environments/getenvironment.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var getEnvironmentCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]"}, +} + +// initGetEnvironmentCmd initializes the get-environment command. +func initGetEnvironmentCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Gets an environment.", + Long: "Gets an environment.", + Example: " gemini-api environments get --id ", + Args: cobra.NoArgs, + RunE: runGetEnvironmentCmd, + Annotations: map[string]string{ + "speakeasy_operation": "GetEnvironment", + }, + } + flagutil.RegisterFlags(cmd, getEnvironmentCmdMeta) + if err := flagutil.ValidateMeta[operations.GetEnvironmentRequest](getEnvironmentCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for get-environment: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runGetEnvironmentCmd executes the get-environment command. +func runGetEnvironmentCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetEnvironmentRequest](cmd, getEnvironmentCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Environments.GetEnvironment(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/environments/internal/root.go b/internal/cli/environments/internal/root.go new file mode 100644 index 0000000..1a5275b --- /dev/null +++ b/internal/cli/environments/internal/root.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package internal + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitInternalRoot(parent *cobra.Command) error { + var InternalCmd = &cobra.Command{ + Use: "internal", + Short: "Operations for internal", + Long: "Operations for internal", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initStartUploadCmd(InternalCmd); err != nil { + return err + } + + parent.AddCommand(InternalCmd) + return nil +} diff --git a/internal/cli/environments/internal/startupload.go b/internal/cli/environments/internal/startupload.go new file mode 100644 index 0000000..9dcb8ec --- /dev/null +++ b/internal/cli/environments/internal/startupload.go @@ -0,0 +1,95 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package internal + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var startUploadCmdMeta = []flagutil.FlagMeta{ + {FlagName: "environment", FieldPath: "Environment", Kind: flagutil.FlagKindString, Required: true, Description: "The ID of the environment that owns the destination file. [required]"}, + {FlagName: "path", Shorthand: "p", FieldPath: "Path", Kind: flagutil.FlagKindString, Required: true, Description: "The relative destination path inside the environment workspace. [required]"}, + {FlagName: "extract", FieldPath: "Extract", Kind: flagutil.FlagKindBool, Optional: true, Description: "Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`."}, + {FlagName: "overwrite", FieldPath: "Overwrite", Kind: flagutil.FlagKindBool, Optional: true, Description: "Optional. Whether to overwrite the destination file if it already exists."}, + {FlagName: "x-goog-upload-header-content-length", FieldPath: "XGoogUploadHeaderContentLength", Kind: flagutil.FlagKindInt64, Required: true, HasMinimum: true, Minimum: 0, Description: "Total number of file bytes that will be uploaded to the session URL. [required]"}, + {FlagName: "x-goog-upload-header-content-type", FieldPath: "XGoogUploadHeaderContentType", Kind: flagutil.FlagKindString, Required: true, MinLength: 1, Description: "MIME type of the file that will be uploaded to the session URL. [required]"}, +} + +// initStartUploadCmd initializes the start-upload command. +func initStartUploadCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "start-upload", + Short: "Start an environment file upload", + Long: "Starts a resumable upload session for a file in an environment workspace.\nUpload the file bytes to the URL returned in the `X-Goog-Upload-URL`\nresponse header, using the resumable upload protocol.", + Example: "", + Args: cobra.NoArgs, + RunE: runStartUploadCmd, + Aliases: []string{"su"}, + Annotations: map[string]string{ + "speakeasy_operation": "StartEnvironmentFileUpload", + }, + } + flagutil.RegisterFlags(cmd, startUploadCmdMeta) + if err := flagutil.ValidateMeta[operations.StartEnvironmentFileUploadRequest](startUploadCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for start-upload: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runStartUploadCmd executes the start-upload command. +func runStartUploadCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.StartEnvironmentFileUploadRequest](cmd, startUploadCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Environments.Internal.StartUpload(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/environments/listenvironments.go b/internal/cli/environments/listenvironments.go new file mode 100644 index 0000000..6cc46c6 --- /dev/null +++ b/internal/cli/environments/listenvironments.go @@ -0,0 +1,90 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listEnvironmentsCmdMeta = []flagutil.FlagMeta{ + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. Maximum number of environments to return.\\nIf unspecified, defaults to 50. Maximum is 1000."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Pagination token."}, +} + +// initListEnvironmentsCmd initializes the list-environments command. +func initListEnvironmentsCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "Lists environments.", + Long: "Lists environments.", + Example: " gemini-api environments list", + Args: cobra.NoArgs, + RunE: runListEnvironmentsCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ListEnvironments", + }, + } + flagutil.RegisterFlags(cmd, listEnvironmentsCmdMeta) + if err := flagutil.ValidateMeta[operations.ListEnvironmentsRequest](listEnvironmentsCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list-environments: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runListEnvironmentsCmd executes the list-environments command. +func runListEnvironmentsCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.ListEnvironmentsRequest](cmd, listEnvironmentsCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Environments.ListEnvironments(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/environments/root.go b/internal/cli/environments/root.go new file mode 100644 index 0000000..45ac7f7 --- /dev/null +++ b/internal/cli/environments/root.go @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "github.com/google-gemini/gemini-api-cli/internal/cli/environments/environmentsfiles" + "github.com/google-gemini/gemini-api-cli/internal/cli/environments/internal" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitEnvironmentsRoot(parent *cobra.Command) error { + var EnvironmentsCmd = &cobra.Command{ + Use: "environments", + Short: "Operations for environments", + Long: "Operations for environments", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := internal.InitInternalRoot(EnvironmentsCmd); err != nil { + return err + } + if err := environmentsfiles.InitEnvironmentsFilesRoot(EnvironmentsCmd); err != nil { + return err + } + + if err := initListEnvironmentsCmd(EnvironmentsCmd); err != nil { + return err + } + + if err := initCreateEnvironmentCmd(EnvironmentsCmd); err != nil { + return err + } + + if err := initDeleteEnvironmentCmd(EnvironmentsCmd); err != nil { + return err + } + + if err := initGetEnvironmentCmd(EnvironmentsCmd); err != nil { + return err + } + + parent.AddCommand(EnvironmentsCmd) + return nil +} diff --git a/internal/cli/files/filesdelete.go b/internal/cli/files/filesdelete.go new file mode 100644 index 0000000..094a6df --- /dev/null +++ b/internal/cli/files/filesdelete.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package files + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var filesDeleteCmdMeta = []flagutil.FlagMeta{ + {FlagName: "file", Shorthand: "f", FieldPath: "File", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, +} + +// initFilesDeleteCmd initializes the files-delete command. +func initFilesDeleteCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete", + Short: "Deletes the `File`.", + Long: "Deletes the `File`.", + Example: "", + Args: cobra.NoArgs, + RunE: runFilesDeleteCmd, + Annotations: map[string]string{ + "speakeasy_operation": "FilesDelete", + }, + } + flagutil.RegisterFlags(cmd, filesDeleteCmdMeta) + if err := flagutil.ValidateMeta[operations.FilesDeleteRequest](filesDeleteCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for files-delete: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runFilesDeleteCmd executes the files-delete command. +func runFilesDeleteCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.FilesDeleteRequest](cmd, filesDeleteCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Files.FilesDelete(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/files/filesget.go b/internal/cli/files/filesget.go new file mode 100644 index 0000000..c444d5f --- /dev/null +++ b/internal/cli/files/filesget.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package files + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var filesGetCmdMeta = []flagutil.FlagMeta{ + {FlagName: "file", Shorthand: "f", FieldPath: "File", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, +} + +// initFilesGetCmd initializes the files-get command. +func initFilesGetCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Gets the metadata for the given `File`.", + Long: "Gets the metadata for the given `File`.", + Example: "", + Args: cobra.NoArgs, + RunE: runFilesGetCmd, + Annotations: map[string]string{ + "speakeasy_operation": "FilesGet", + }, + } + flagutil.RegisterFlags(cmd, filesGetCmdMeta) + if err := flagutil.ValidateMeta[operations.FilesGetRequest](filesGetCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for files-get: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runFilesGetCmd executes the files-get command. +func runFilesGetCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.FilesGetRequest](cmd, filesGetCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Files.FilesGet(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/files/fileslist.go b/internal/cli/files/fileslist.go new file mode 100644 index 0000000..f3b5532 --- /dev/null +++ b/internal/cli/files/fileslist.go @@ -0,0 +1,125 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package files + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var filesListCmdMeta = []flagutil.FlagMeta{ + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. Maximum number of `File`s to return per page. If unspecified, defaults to 10. Maximum `page_size` is 100."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. A page token from a previous `ListFiles` call."}, +} + +// initFilesListCmd initializes the files-list command. +func initFilesListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "Lists the metadata for `File`s owned by the requesting project.", + Long: "Lists the metadata for `File`s owned by the requesting project.", + Example: " gemini-api files list", + Args: cobra.NoArgs, + RunE: runFilesListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "FilesList", + }, + } + flagutil.RegisterFlags(cmd, filesListCmdMeta) + if err := flagutil.ValidateMeta[operations.FilesListRequest](filesListCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for files-list: %w", err) + } + cmd.Flags().BoolP("all", "a", false, "Automatically paginate and fetch all results (streams NDJSON for JSON output)") + cmd.Flags().Int("max-pages", 0, "Maximum number of pages to fetch when using --all (0 = no limit)") + parent.AddCommand(cmd) + return nil +} + +// runFilesListCmd executes the files-list command. +func runFilesListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + allPages, _ := flagutil.GetBoolFlag(cmd, "all") + maxPages, _ := flagutil.GetIntFlag(cmd, "max-pages") + if maxPages < 0 { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages must be zero or greater")) + } + if flagutil.FlagChanged(cmd, "max-pages") && !allPages { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages requires --all")) + } + req, err := flagutil.BuildRequest[operations.FilesListRequest](cmd, filesListCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if allPages && !client.IsDryRun(cmd) { + res, err := s.Files.FilesList(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.PaginatedResult(cmd, res, "ListFilesResponse", "", maxPages, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.nextPageToken", + NextURL: "", + Results: "", + HasLimit: false, + }) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Files.FilesList(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + morePages := output.HasMorePages(res, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.nextPageToken", + NextURL: "", + Results: "", + HasLimit: false, + }) + + if err := output.Result(cmd, res); err != nil { + return err + } + if morePages && !client.IsDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), "Hint: more pages available. Use --all to fetch all results, or --page-token for manual pagination.") + } + return nil +} diff --git a/internal/cli/files/filesregister.go b/internal/cli/files/filesregister.go new file mode 100644 index 0000000..df06a29 --- /dev/null +++ b/internal/cli/files/filesregister.go @@ -0,0 +1,100 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package files + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var filesRegisterCmdMeta = []flagutil.FlagMeta{ + {FlagName: "uris", Shorthand: "u", FieldPath: "Body.Uris", Kind: flagutil.FlagKindStringArray, Optional: true, Description: "Required. The Google Cloud Storage URIs to register. Example: `gs://bucket/object`."}, +} + +// initFilesRegisterCmd initializes the files-register command. +func initFilesRegisterCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "register", + Short: "Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails.", + Long: "Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails.", + Example: " gemini-api files register", + Args: cobra.NoArgs, + RunE: runFilesRegisterCmd, + Annotations: map[string]string{ + "speakeasy_operation": "FilesRegister", + }, + } + flagutil.RegisterFlags(cmd, filesRegisterCmdMeta) + if err := flagutil.ValidateMeta[operations.FilesRegisterRequest](filesRegisterCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for files-register: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, filesRegisterCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for files-register: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runFilesRegisterCmd executes the files-register command. +func runFilesRegisterCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "FilesRegister") + } + req, err := flagutil.BuildRequest[operations.FilesRegisterRequest](cmd, filesRegisterCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Files.FilesRegister(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/files/root.go b/internal/cli/files/root.go new file mode 100644 index 0000000..18806e8 --- /dev/null +++ b/internal/cli/files/root.go @@ -0,0 +1,57 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package files + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitFilesRoot(parent *cobra.Command) error { + var FilesCmd = &cobra.Command{ + Use: "files", + Short: "Upload / list / download / delete media (48h TTL)", + Long: "Upload / list / download / delete media (48h TTL)", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initFilesListCmd(FilesCmd); err != nil { + return err + } + + if err := initFilesDeleteCmd(FilesCmd); err != nil { + return err + } + + if err := initFilesGetCmd(FilesCmd); err != nil { + return err + } + + if err := initFilesRegisterCmd(FilesCmd); err != nil { + return err + } + + parent.AddCommand(FilesCmd) + return nil +} diff --git a/internal/cli/intents.go b/internal/cli/intents.go new file mode 100644 index 0000000..9364069 --- /dev/null +++ b/internal/cli/intents.go @@ -0,0 +1,318 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "fmt" + "sort" + + "github.com/google-gemini/gemini-api-cli/internal/cli/agent" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func init() { + cobra.EnableCommandSorting = false +} + +func initIntentCmds(rootCmd *cobra.Command) error { + rootCmd.AddGroup(&cobra.Group{ID: "create", Title: "Create:"}) + rootCmd.AddGroup(&cobra.Group{ID: "understand", Title: "Understand:"}) + rootCmd.AddGroup(&cobra.Group{ID: "manage", Title: "Manage:"}) + rootCmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced:"}) + if parent := findCommandByPath(rootCmd, []string{"agent"}); parent == nil { + return fmt.Errorf("declared parent %q for intent command %q does not exist in the generated CLI", "agent", "agent-run") + } else { + if err := agent.InitIntentAgentRun(parent); err != nil { + return fmt.Errorf("init intent agent-run: %w", err) + } + } + if err := agent.InitIntentGenerate(rootCmd); err != nil { + return fmt.Errorf("init intent generate: %w", err) + } + if err := setCommandGroup(rootCmd, "generate", "create"); err != nil { + return err + } + if err := agent.InitIntentImage(rootCmd); err != nil { + return fmt.Errorf("init intent image: %w", err) + } + if err := setCommandGroup(rootCmd, "image", "create"); err != nil { + return err + } + if err := agent.InitIntentMusic(rootCmd); err != nil { + return fmt.Errorf("init intent music: %w", err) + } + if err := setCommandGroup(rootCmd, "music", "create"); err != nil { + return err + } + if err := agent.InitIntentVideo(rootCmd); err != nil { + return fmt.Errorf("init intent video: %w", err) + } + if err := setCommandGroup(rootCmd, "video", "create"); err != nil { + return err + } + if owner := findOwningCommand(rootCmd, "analyze"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("analyze", "Ask questions about video, audio, PDF, or image files", "", "understand", "\"analyze\" is supported by the Interactions API (image, audio, document, and video content inputs) but needs the CLI's file-input adapter, which is not in this build yet. Meanwhile pass file content parts via \"gemini-api agent run --body\"")) + } else { + owner.GroupID = "understand" + } + if owner := findOwningCommand(rootCmd, "batch"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("batch", "Async batch jobs at reduced cost", "", "manage", "\"batch\" needs the classic GenAI Batches API, which is not part of this interactions-only build")) + } else { + owner.GroupID = "manage" + } + if owner := findOwningCommand(rootCmd, "docs"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("docs", "Gemini API documentation & guides", "", "advanced", "\"docs\" curated guides are not part of this build yet. Meanwhile browse https://ai.google.dev/gemini-api/docs")) + } else { + owner.GroupID = "advanced" + } + if owner := findOwningCommand(rootCmd, "embed"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("embed", "Vector embeddings (gemini-embedding-2)", "", "understand", "\"embed\" needs the classic GenAI API surface, which is not part of this interactions-only build")) + } else { + owner.GroupID = "understand" + } + if owner := findOwningCommand(rootCmd, "files"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("files", "Upload / list / download / delete media (48h TTL)", "", "manage", "\"files\" needs the classic GenAI Files API, which is not part of this interactions-only build")) + } else { + owner.GroupID = "manage" + } + if owner := findOwningCommand(rootCmd, "tokens"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("tokens", "Count tokens without generating", "", "understand", "\"tokens\" needs the classic GenAI API surface, which is not part of this interactions-only build")) + } else { + owner.GroupID = "understand" + } + if owner := findOwningCommand(rootCmd, "transcribe"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("transcribe", "Audio/video → text (timestamps, captions)", "", "understand", "\"transcribe\" is supported by the Interactions API (audio and video content inputs) but needs the CLI's file-input adapter, which is not in this build yet. Meanwhile pass file content parts via \"gemini-api agent run --body\"")) + } else { + owner.GroupID = "understand" + } + if owner := findOwningCommand(rootCmd, "tts"); owner == nil { + rootCmd.AddCommand(newPlannedCmd("tts", "Text to speech (gemini-3.1-flash-tts-preview)", "", "create", "\"tts\" is not yet callable through the Interactions API — the TTS models (gemini-3.1-flash-tts-preview) reject interaction requests and audio-modality speech needs the classic generateContent speech config. Verified live 2026-08-13")) + } else { + owner.GroupID = "create" + } + if err := setCommandGroup(rootCmd, "agent", "manage"); err != nil { + return err + } + if err := setCommandGroup(rootCmd, "configure", "manage"); err != nil { + return err + } + if err := setCommandGroup(rootCmd, "models", "manage"); err != nil { + return err + } + if err := setCommandGroup(rootCmd, "triggers", "advanced"); err != nil { + return err + } + if err := setCommandGroup(rootCmd, "webhooks", "advanced"); err != nil { + return err + } + + return nil +} + +func newPlannedCmd(name, short, long, groupID, note string) *cobra.Command { + cmd := &cobra.Command{ + Use: name, + Short: short, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return output.WithCLIReason(fmt.Errorf("%s", note), output.ReasonCLIUnavailable) + }, + } + unavailable := "Not yet available: " + note + "." + if long != "" { + cmd.Long = long + "\n\n" + unavailable + } else { + cmd.Long = unavailable + } + if groupID != "" { + cmd.GroupID = groupID + } + return cmd +} + +func newCustomCmd(name, short, long, groupID string) *cobra.Command { + cmd := &cobra.Command{ + Use: name, + Short: short, + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return fmt.Errorf("command %q requires a hand-written implementation that was not registered", name) + }, + } + if long != "" { + cmd.Long = long + } + if groupID != "" { + cmd.GroupID = groupID + } + usage.MarkDynamic(cmd) + return cmd +} + +func applyCompactHelpAnnotations(cmd *cobra.Command, defaults, learn, escalate string) { + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + if defaults != "" { + cmd.Annotations["speakeasy_help_defaults"] = defaults + } + if learn != "" { + cmd.Annotations["speakeasy_help_learn"] = learn + } + if escalate != "" { + cmd.Annotations["speakeasy_help_escalate"] = escalate + } +} + +type intentOrderEntry struct { + ParentPath []string + Name string +} + +var intentDeclarationOrder = []intentOrderEntry{ + {ParentPath: []string{}, Name: "agent"}, + {ParentPath: []string{"agent"}, Name: "run"}, + {ParentPath: []string{}, Name: "analyze"}, + {ParentPath: []string{}, Name: "batch"}, + {ParentPath: []string{}, Name: "configure"}, + {ParentPath: []string{}, Name: "docs"}, + {ParentPath: []string{}, Name: "embed"}, + {ParentPath: []string{}, Name: "files"}, + {ParentPath: []string{}, Name: "generate"}, + {ParentPath: []string{}, Name: "image"}, + {ParentPath: []string{}, Name: "models"}, + {ParentPath: []string{}, Name: "music"}, + {ParentPath: []string{}, Name: "tokens"}, + {ParentPath: []string{}, Name: "transcribe"}, + {ParentPath: []string{}, Name: "triggers"}, + {ParentPath: []string{}, Name: "tts"}, + {ParentPath: []string{}, Name: "video"}, + {ParentPath: []string{}, Name: "webhooks"}, +} + +func applyDeclaredCommandOrder(rootCmd *cobra.Command) { + applyCommandOrderEntries(rootCmd, intentDeclarationOrder) +} + +func applyCommandOrderEntries(rootCmd *cobra.Command, order []intentOrderEntry) { + byParent := map[*cobra.Command][]string{} + for _, entry := range order { + parent := findCommandByPath(rootCmd, entry.ParentPath) + if parent == nil { + parent = rootCmd + } + byParent[parent] = append(byParent[parent], entry.Name) + } + + var reorder func(*cobra.Command) + reorder = func(parent *cobra.Command) { + children := parent.Commands() + if len(children) == 0 { + return + } + // First-added wins the name: cobra's builtin help and completion + // commands are appended after the declared tree (InitDefaultHelpCmd + // runs after initIntentCmds, and again inside Execute), so a declared + // command that shares a builtin's name must keep its slot here. + // Letting the later-added builtin claim the map entry would drop the + // declared command from the re-added list entirely — a silently + // swallowed command. Cobra dispatches to the earliest match, so + // keeping the declared command first also keeps it runnable. + byName := map[string]*cobra.Command{} + for _, child := range children { + if _, ok := byName[child.Name()]; !ok { + byName[child.Name()] = child + } + } + var reordered []*cobra.Command + taken := map[*cobra.Command]bool{} + for _, name := range byParent[parent] { + if child, ok := byName[name]; ok && !taken[child] { + reordered = append(reordered, child) + taken[child] = true + } + } + var rest []*cobra.Command + for _, child := range children { + if !taken[child] { + rest = append(rest, child) + } + } + sort.SliceStable(rest, func(i, j int) bool { return rest[i].Name() < rest[j].Name() }) + reordered = append(reordered, rest...) + parent.RemoveCommand(children...) + parent.AddCommand(reordered...) + for _, child := range reordered { + reorder(child) + } + } + reorder(rootCmd) +} + +func findCommandByPath(rootCmd *cobra.Command, path []string) *cobra.Command { + current := rootCmd + for _, segment := range path { + var next *cobra.Command + for _, child := range current.Commands() { + if child.Name() == segment { + next = child + break + } + } + if next == nil { + return nil + } + current = next + } + return current +} + +// Cobra panics in ExecuteC when a child names a group its parent does not declare. +func ensureCommandGroup(cmd *cobra.Command, id, title string) { + if !cmd.ContainsGroup(id) { + cmd.AddGroup(&cobra.Group{ID: id, Title: title}) + } +} + +func setCommandGroup(rootCmd *cobra.Command, name string, groupID string) error { + if owner := findOwningCommand(rootCmd, name); owner != nil { + owner.GroupID = groupID + return nil + } + return fmt.Errorf("declared command group %q does not exist in the generated CLI", name) +} + +func findOwningCommand(parentCmd *cobra.Command, name string) *cobra.Command { + for _, c := range parentCmd.Commands() { + if c.Name() == name || c.HasAlias(name) { + return c + } + } + return nil +} + +func commandExists(parentCmd *cobra.Command, name string) bool { + return findOwningCommand(parentCmd, name) != nil +} diff --git a/internal/cli/masking.go b/internal/cli/masking.go new file mode 100644 index 0000000..4743a01 --- /dev/null +++ b/internal/cli/masking.go @@ -0,0 +1,56 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "bufio" + "os" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/term" +) + +// maskSecret returns a masked version of a secret for display. +// It preserves the first 2 and last 2 characters with a fixed-length +// mask in between to avoid leaking the secret's exact length. +func maskSecret(s string) string { + if s == "" { + return "(not set)" + } + if len(s) <= 4 { + return "****" + } + return s[:2] + "******" + s[len(s)-2:] +} + +// readSecret reads a secret value from the user. When stdin is a terminal, +// it uses term.ReadPassword for hidden input. Otherwise (pipes, tests), +// it falls back to reading a line from cmd.InOrStdin() as plain text. +func readSecret(cmd *cobra.Command) ([]byte, error) { + if f, ok := cmd.InOrStdin().(*os.File); ok && term.IsTerminal(int(f.Fd())) { + return term.ReadPassword(int(f.Fd())) + } + + // Non-terminal fallback: read a line from cmd input (supports test harness piped input) + reader := bufio.NewReader(cmd.InOrStdin()) + line, err := reader.ReadString('\n') + if err != nil && line == "" { + return nil, err + } + return []byte(strings.TrimRight(line, "\r\n")), nil +} diff --git a/internal/cli/models/modelsget.go b/internal/cli/models/modelsget.go new file mode 100644 index 0000000..bd1887e --- /dev/null +++ b/internal/cli/models/modelsget.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package models + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var modelsGetCmdMeta = []flagutil.FlagMeta{ + {FlagName: "model", Shorthand: "m", FieldPath: "Model", Kind: flagutil.FlagKindString, Required: true, Description: "[required]"}, +} + +// initModelsGetCmd initializes the models-get command. +func initModelsGetCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information.", + Long: "Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information.", + Example: " gemini-api models get --model Prius", + Args: cobra.NoArgs, + RunE: runModelsGetCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ModelsGet", + }, + } + flagutil.RegisterFlags(cmd, modelsGetCmdMeta) + if err := flagutil.ValidateMeta[operations.ModelsGetRequest](modelsGetCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for models-get: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runModelsGetCmd executes the models-get command. +func runModelsGetCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.ModelsGetRequest](cmd, modelsGetCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Models.ModelsGet(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/models/modelslist.go b/internal/cli/models/modelslist.go new file mode 100644 index 0000000..e03efd9 --- /dev/null +++ b/internal/cli/models/modelslist.go @@ -0,0 +1,125 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package models + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var modelsListCmdMeta = []flagutil.FlagMeta{ + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "The maximum number of `Models` to return (per page). If unspecified, 50 models will be returned per page. This method returns at most 1000 models per page, even if you pass a larger page_size."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "A page token, received from a previous `ListModels` call. Provide the `page_token` returned by one request as an argument to the next request to retrieve the next page. When paginating, all other parameters provided to `ListModels` must match the call that provided the page token."}, +} + +// initModelsListCmd initializes the models-list command. +func initModelsListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API.", + Long: "Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API.", + Example: " gemini-api models list", + Args: cobra.NoArgs, + RunE: runModelsListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ModelsList", + }, + } + flagutil.RegisterFlags(cmd, modelsListCmdMeta) + if err := flagutil.ValidateMeta[operations.ModelsListRequest](modelsListCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for models-list: %w", err) + } + cmd.Flags().BoolP("all", "a", false, "Automatically paginate and fetch all results (streams NDJSON for JSON output)") + cmd.Flags().Int("max-pages", 0, "Maximum number of pages to fetch when using --all (0 = no limit)") + parent.AddCommand(cmd) + return nil +} + +// runModelsListCmd executes the models-list command. +func runModelsListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + allPages, _ := flagutil.GetBoolFlag(cmd, "all") + maxPages, _ := flagutil.GetIntFlag(cmd, "max-pages") + if maxPages < 0 { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages must be zero or greater")) + } + if flagutil.FlagChanged(cmd, "max-pages") && !allPages { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages requires --all")) + } + req, err := flagutil.BuildRequest[operations.ModelsListRequest](cmd, modelsListCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if allPages && !client.IsDryRun(cmd) { + res, err := s.Models.ModelsList(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.PaginatedResult(cmd, res, "ListModelsResponse", "", maxPages, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.nextPageToken", + NextURL: "", + Results: "", + HasLimit: false, + }) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Models.ModelsList(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + morePages := output.HasMorePages(res, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.nextPageToken", + NextURL: "", + Results: "", + HasLimit: false, + }) + + if err := output.Result(cmd, res); err != nil { + return err + } + if morePages && !client.IsDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), "Hint: more pages available. Use --all to fetch all results, or --page-token for manual pagination.") + } + return nil +} diff --git a/internal/cli/models/root.go b/internal/cli/models/root.go new file mode 100644 index 0000000..74e9227 --- /dev/null +++ b/internal/cli/models/root.go @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package models + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitModelsRoot(parent *cobra.Command) error { + var ModelsCmd = &cobra.Command{ + Use: "models", + Short: "Full model operations — list and get model metadata, embed, count tokens, and generate with complete request control", + Long: "Full model operations — list and get model metadata, embed, count tokens, and generate with complete request control", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initModelsListCmd(ModelsCmd); err != nil { + return err + } + + if err := initModelsGetCmd(ModelsCmd); err != nil { + return err + } + + parent.AddCommand(ModelsCmd) + return nil +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..3ea9c9a --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,654 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/cli/agent" + "github.com/google-gemini/gemini-api-cli/internal/cli/credentials" + "github.com/google-gemini/gemini-api-cli/internal/cli/custom" + "github.com/google-gemini/gemini-api-cli/internal/cli/environments" + "github.com/google-gemini/gemini-api-cli/internal/cli/files" + "github.com/google-gemini/gemini-api-cli/internal/cli/models" + "github.com/google-gemini/gemini-api-cli/internal/cli/triggers" + "github.com/google-gemini/gemini-api-cli/internal/cli/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/clierrors" + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/explorer" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/interactive" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var rootCmd *cobra.Command + +// NewRootCommand creates and returns the root command. +// This is exported for testing purposes to allow in-process command execution. +func NewRootCommand() (*cobra.Command, error) { + cobra.AddTemplateFunc("groupedFlagUsages", groupedFlagUsages) + cobra.AddTemplateFunc("groupedGlobalFlagUsages", groupedGlobalFlagUsages) + cobra.AddTemplateFunc("compactHelpDefaults", compactHelpDefaults) + cobra.AddTemplateFunc("compactHelpFooter", compactHelpFooter) + cobra.AddTemplateFunc("compactRootHelpFooter", compactRootHelpFooter) + rootCmd := &cobra.Command{ + Use: "gemini-api", + Short: "Gemini API: Use the Gemini Interactions API and managed-agent platform from the command line", + Long: "Gemini API: Use the Gemini Interactions API and managed-agent platform from the command line.\n\nGet started:\n Set GEMINI_API_KEY, or run: gemini-api configure\n Then run a model or managed agent: gemini-api agent --help\n Add --dry-run to preview any API call without sending it.", + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if compactHelpGlobalRequested(cmd) { + out := cmd.OutOrStdout() + fmt.Fprintln(out, "Global flags (apply to every command):") + fmt.Fprintln(out) + _, err := fmt.Fprintln(out, renderGroupedFlags(cmd.Root().PersistentFlags(), "Global Flags")) + return err + } + return cmd.Help() + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if compactHelpGlobalRequested(cmd) { + return nil + } + if usage.UsageRequested(cmd) { + return nil + } + if err := flagutil.ValidateEnumFlag(cmd, "output-format", output.Formats); err != nil { + return err + } + if err := flagutil.ValidateEnumFlag(cmd, "color", []string{"auto", "always", "never"}); err != nil { + return err + } + if err := config.Init("gemini-api", "GEMINI"); err != nil { + return err + } + output.InitAgentMode(cmd) + flagutil.SetStdinReadDeadline(output.IsAgentMode()) + return nil + }, + } + rootCmd.Flags().Bool("help-global", false, "Print global flags shared by every command") + if err := environments.InitEnvironmentsRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init environments: %w", err) + } + if err := agent.InitAgentRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init agent: %w", err) + } + if err := credentials.InitCredentialsRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init credentials: %w", err) + } + if err := files.InitFilesRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init files: %w", err) + } + if err := models.InitModelsRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init models: %w", err) + } + if err := triggers.InitTriggersRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init triggers: %w", err) + } + if err := webhooks.InitWebhooksRoot(rootCmd); err != nil { + return nil, fmt.Errorf("init webhooks: %w", err) + } + if err := initConfigureCmd(rootCmd); err != nil { + return nil, fmt.Errorf("init configure: %w", err) + } + if err := initWhoamiCmd(rootCmd); err != nil { + return nil, fmt.Errorf("init whoami: %w", err) + } + if err := initVersionCmd(rootCmd); err != nil { + return nil, fmt.Errorf("init version: %w", err) + } + if err := initAuthCmd(rootCmd); err != nil { + return nil, fmt.Errorf("init auth: %w", err) + } + initExploreCmd(rootCmd) + if err := initCatalogCmds(rootCmd); err != nil { + return nil, fmt.Errorf("init catalogs: %w", err) + } + + // Global output format flag + rootCmd.PersistentFlags().StringP("output-format", "o", "pretty", "Specify the output format. Options: "+strings.Join(output.Formats, ", ")+".") + + // Color control flag + rootCmd.PersistentFlags().String("color", "auto", "Control colored output: auto (color when output is a TTY), always, or never. Respects NO_COLOR and FORCE_COLOR env vars.") + + // jq filtering flag + rootCmd.PersistentFlags().StringP("jq", "q", "", "Filter and transform output using a jq expression (e.g., '.name', '.items[] | .id')") + rootCmd.PersistentFlags().Bool("raw-output", true, "Write --jq string results as raw text instead of JSON strings (like jq -r); non-string results stay JSON") + + // Global server URL flag + rootCmd.PersistentFlags().String("server-url", "", "Override the default server URL") + + // Custom header flag (repeatable: --header "Key: Value" --header "Key2: Value2") + rootCmd.PersistentFlags().StringArrayP("header", "H", nil, "Set a custom HTTP request header (format: \"Key: Value\"). Can be specified multiple times.") + + // Response headers flag + rootCmd.PersistentFlags().Bool("include-headers", false, "Include HTTP response headers in the output") + + // Request timeout (always available) + rootCmd.PersistentFlags().String("timeout", "", "HTTP request timeout (e.g., 30s, 5m, 100ms)") + rootCmd.PersistentFlags().Bool("interactive", false, "Prompt for missing inputs and open guided configure/auth forms (forms fall back to line prompts on stdin off-TTY)") + rootCmd.PersistentFlags().Bool("no-interactive", false, "Disable all interactive features (auto-prompting, explorer auto-launch, TUI forms)") + + // Diagnostics flags + rootCmd.PersistentFlags().Bool("usage", false, "Print the CLI Usage schema in KDL format") + rootCmd.PersistentFlags().Bool("dry-run", false, "Preview API requests without sending them (no network, no OS keychain). Human preview on stderr; with -o json or --jq, one JSON object per request on stdout. Local mutation commands (auth login, auth logout and configure) make no request: they skip prompts and writes and report a no-op (stderr, or one JSON object on stdout in the machine form)") + rootCmd.PersistentFlags().BoolP("debug", "d", false, "Log request and response diagnostics to stderr") + + // Agent mode — optimized output for AI coding agent consumption. + rootCmd.PersistentFlags().Bool("agent-mode", false, + "Enable structured errors and default TOON output for AI coding agents.") + // Retry control flags + // Defaults: exponential backoff, 500ms initial / 10s max interval, 1.5x exponent, 30s max elapsed time. + rootCmd.PersistentFlags().Bool("no-retries", false, "Disable automatic retries (default: retries enabled with exponential backoff)") + rootCmd.PersistentFlags().String("retry-max-elapsed-time", "", "Maximum total time for retries (e.g., 30s, 5m). Default: 30s") + rootCmd.PersistentFlags().Bool("retry-connection-errors", false, "Retry on connection errors (EOF, reset, etc.)") + rootCmd.PersistentFlags().String("retry-config", "", `Full retry config as JSON. Schema: {"strategy":"backoff","backoff":{"initialInterval":500,"maxInterval":10000,"exponent":1.5,"maxElapsedTime":30000},"retryConnectionErrors":false}. Use strategy "attempt-count-backoff" with maxRetries for attempt-count retries. Times are in milliseconds.`) + for _, flag := range []string{"no-retries", "retry-max-elapsed-time", "retry-connection-errors", "retry-config"} { + _ = rootCmd.PersistentFlags().MarkHidden(flag) + } + + // Global security flags + rootCmd.PersistentFlags().String("api-key", "", "Gemini API key sent as x-goog-api-key.") + _ = rootCmd.PersistentFlags().SetAnnotation("api-key", "speakeasy:group", []string{"Authentication"}) + rootCmd.PersistentFlags().String("access-token", "", "OAuth access token sent as a bearer Authorization header.") + _ = rootCmd.PersistentFlags().SetAnnotation("access-token", "speakeasy:group", []string{"Authentication"}) + // Global parameter flags + rootCmd.PersistentFlags().String("api-version", "v1beta", "Which version of the API to use (env: GEMINI_API_VERSION)") + _ = rootCmd.PersistentFlags().SetAnnotation("api-version", "speakeasy:group", []string{"API Parameters"}) + rootCmd.PersistentFlags().String("api-revision", "", "Interactions API revision to request (env: GEMINI_API_REVISION)") + _ = rootCmd.PersistentFlags().SetAnnotation("api-revision", "speakeasy:group", []string{"API Parameters"}) + rootCmd.PersistentFlags().String("user-project", "", "Quota project header to send with Google GenAI API requests (env: GEMINI_USER_PROJECT)") + _ = rootCmd.PersistentFlags().SetAnnotation("user-project", "speakeasy:group", []string{"API Parameters"}) + + // Annotate persistent flags for grouped help display + for _, ga := range []struct{ flag, group string }{ + {"output-format", "Output"}, + {"color", "Output"}, + {"raw-output", "Output"}, + {"jq", "Output"}, + {"include-headers", "Output"}, + {"interactive", "Output"}, + {"no-interactive", "Output"}, + {"server-url", "Server"}, + {"header", "Network"}, + {"timeout", "Network"}, + {"usage", "Diagnostics"}, + {"dry-run", "Diagnostics"}, + {"debug", "Diagnostics"}, + {"agent-mode", "Diagnostics"}, + } { + _ = rootCmd.PersistentFlags().SetAnnotation(ga.flag, "speakeasy:group", []string{ga.group}) + } + for _, flag := range []string{"no-retries", "retry-max-elapsed-time", "retry-connection-errors", "retry-config"} { + _ = rootCmd.PersistentFlags().SetAnnotation(flag, "speakeasy:group", []string{"Network"}) + } + + rootCmd.SetUsageTemplate(groupedUsageTemplate()) + + if err := initIntentCmds(rootCmd); err != nil { + return nil, err + } + + custom.Register(rootCmd) + + // Cobra creates its default help and completion commands lazily inside Execute. + rootCmd.InitDefaultHelpCmd() + rootCmd.InitDefaultCompletionCmd() + applyDeclaredCommandOrder(rootCmd) + interactive.Intercept(rootCmd) + usage.Intercept(rootCmd) + // Cobra validates Args before any PersistentPreRunE runs. + output.InstallErrorHandling(rootCmd) + + return rootCmd, nil +} + +func Execute() error { + var err error + rootCmd, err = NewRootCommand() + if err != nil { + return err + } + + return ExecuteRoot(context.Background(), rootCmd, os.Args[1:]) +} + +func ExecuteRoot(ctx context.Context, root *cobra.Command, args []string) error { + // Cobra aborts on an unknown command or flag before PersistentPreRunE runs. + output.InitAgentMode(root) + target, _, findErr := root.Find(args) + if findErr != nil || target == nil { + target = root + } + output.PreparseRenderingFlags(target, args) + root.SetArgs(args) + executed, err := root.ExecuteContextC(ctx) + if executed == nil { + executed = root + } + if err != nil && findErr != nil { + err = flagutil.WithCLIValidation(err) + } + return output.CLIError(executed, err) +} + +func initExploreCmd(parent *cobra.Command) { + parent.AddCommand(&cobra.Command{ + Use: "explore", + Short: "Interactively browse and run commands", + Long: "Launch an interactive command explorer to browse available commands, view their descriptions and flags, and execute them.", + RunE: func(cmd *cobra.Command, args []string) error { + // Agent mode: print help instead of launching TUI. + if output.IsAgentMode() { + return cmd.Root().Help() + } + if err := interactive.Resolve(cmd).ValidateDirectExplore(); err != nil { + return err + } + return runExplorer(cmd.Root()) + }, + }) +} + +func ExplorerHandoffArgs(root *cobra.Command, selectedArgs []string) []string { + interactionFlag := "--interactive" + if noInteractive, _ := root.PersistentFlags().GetBool("no-interactive"); noInteractive { + interactionFlag = "--no-interactive" + } + handoffArgs := []string{interactionFlag} + if dryRun, _ := root.PersistentFlags().GetBool("dry-run"); dryRun { + handoffArgs = append(handoffArgs, "--dry-run") + } + if debug, _ := root.PersistentFlags().GetBool("debug"); debug { + handoffArgs = append(handoffArgs, "--debug") + } + // Rendering flags given to the explore invocation apply to the selected + // command: the fresh command tree re-parses argv (and resets the + // preparsed rendering state), so they must travel with it. The + // --name=value form keeps boolean flags from swallowing the next token. + for _, name := range []string{"output-format", "jq", "raw-output", "color"} { + if flag := root.PersistentFlags().Lookup(name); flag != nil && flag.Changed { + handoffArgs = append(handoffArgs, "--"+name+"="+flag.Value.String()) + } + } + return append(handoffArgs, selectedArgs...) +} + +// runExplorer launches the explorer TUI and handles command execution handoff. +func runExplorer(root *cobra.Command) error { + selectedArgs, err := explorer.Run(root, Version) + if err != nil { + return err + } + if selectedArgs == nil { + return nil // user quit + } + // Build a fresh command tree and execute the selected command + freshRoot, err := NewRootCommand() + if err != nil { + return err + } + return ExecuteRoot(context.Background(), freshRoot, ExplorerHandoffArgs(root, selectedArgs)) +} + +// globalFlagGroupOrder defines the display order for flag groups in help output. +// Groups without any flags are skipped automatically. +var globalFlagGroupOrder = []string{"Output", "Authentication", "API Parameters", "Server", "Network", "Diagnostics"} + +// groupedFlagUsages splits local flags into sections for help display. +// For operation commands (flags with "speakeasy:required"): Required/Optional sections. +// For the root command (flags with "speakeasy:group"): category-based sections. +// Falls back to a flat "Flags:" listing otherwise. +func groupedFlagUsages(flags *pflag.FlagSet) string { + required := pflag.NewFlagSet("required", pflag.ContinueOnError) + optional := pflag.NewFlagSet("optional", pflag.ContinueOnError) + hasGroupAnnotation := false + + flags.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + if ann, ok := f.Annotations["speakeasy:required"]; ok && len(ann) > 0 && ann[0] == "true" { + required.AddFlag(f) + } else { + optional.AddFlag(f) + } + if _, ok := f.Annotations["speakeasy:group"]; ok { + hasGroupAnnotation = true + } + }) + + // Operation commands: Required/Optional sections, with sub-groups when present + if required.HasFlags() { + var buf strings.Builder + buf.WriteString("Required Flags:\n") + buf.WriteString(required.FlagUsages()) + if optional.HasFlags() { + if hasGroupAnnotation { + buf.WriteString("\n") + buf.WriteString(renderOperationGroups(optional)) + } else { + buf.WriteString("\nOptional Flags:\n") + buf.WriteString(optional.FlagUsages()) + } + } + return strings.TrimRight(buf.String(), "\n") + } + + // Grouped flags (root command or operation with only optional grouped flags) + if hasGroupAnnotation { + // Check if any groups are in the global order (root-level groups like Output, Server) + hasGlobalGroup := false + flags.VisitAll(func(f *pflag.Flag) { + if ann, ok := f.Annotations["speakeasy:group"]; ok && len(ann) > 0 { + for _, g := range globalFlagGroupOrder { + if ann[0] == g { + hasGlobalGroup = true + } + } + } + }) + if hasGlobalGroup { + return renderGroupedFlags(flags, "Flags") + } + // Operation-level groups (e.g., Address, Profile) + return strings.TrimRight(renderOperationGroups(flags), "\n") + } + + // Fallback: flat listing + return "Flags:\n" + strings.TrimRight(flags.FlagUsages(), "\n") +} + +// groupedGlobalFlagUsages splits inherited (global) flags into category sections +// based on the "speakeasy:group" annotation. Falls back to flat "Global Flags:". +func groupedGlobalFlagUsages(flags *pflag.FlagSet) string { + hasGroupAnnotation := false + flags.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + if _, ok := f.Annotations["speakeasy:group"]; ok { + hasGroupAnnotation = true + } + }) + + if hasGroupAnnotation { + return renderGroupedFlags(flags, "Global Flags") + } + + return "Global Flags:\n" + strings.TrimRight(flags.FlagUsages(), "\n") +} + +// renderOperationGroups renders optional operation flags grouped by their +// "speakeasy:group" annotation. Groups are shown alphabetically, with +// ungrouped flags under a generic "Optional Flags" header. +func renderOperationGroups(flags *pflag.FlagSet) string { + groups := make(map[string]*pflag.FlagSet) + groupOrder := []string{} + groupOrders := make(map[string]int) + ungrouped := pflag.NewFlagSet("ungrouped", pflag.ContinueOnError) + + parseOrder := func(raw string) (int, bool) { + if raw == "" { + return 0, false + } + sign, start := 1, 0 + if raw[0] == '-' { + sign, start = -1, 1 + } + if start == len(raw) { + return 0, false + } + value := 0 + for i := start; i < len(raw); i++ { + if raw[i] < '0' || raw[i] > '9' { + return 0, false + } + value = value*10 + int(raw[i]-'0') + } + return sign * value, true + } + + flags.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + if ann, ok := f.Annotations["speakeasy:group"]; ok && len(ann) > 0 { + name := ann[0] + if _, exists := groups[name]; !exists { + groups[name] = pflag.NewFlagSet(name, pflag.ContinueOnError) + groupOrder = append(groupOrder, name) + } + groups[name].AddFlag(f) + if orderAnn, ok := f.Annotations["speakeasy:group-order"]; ok && len(orderAnn) > 0 { + if order, valid := parseOrder(orderAnn[0]); valid { + current, exists := groupOrders[name] + if !exists || order < current { + groupOrders[name] = order + } + } + } + } else { + ungrouped.AddFlag(f) + } + }) + + for i := 1; i < len(groupOrder); i++ { + for j := i; j > 0; j-- { + current, currentOrdered := groupOrders[groupOrder[j]] + previous, previousOrdered := groupOrders[groupOrder[j-1]] + if !currentOrdered || (previousOrdered && current >= previous) { + break + } + groupOrder[j], groupOrder[j-1] = groupOrder[j-1], groupOrder[j] + } + } + + var buf strings.Builder + + // Show ungrouped optional flags first + if ungrouped.HasFlags() { + buf.WriteString("Optional Flags:\n") + buf.WriteString(ungrouped.FlagUsages()) + } + + // Then show each group + for _, name := range groupOrder { + fs := groups[name] + if !fs.HasFlags() { + continue + } + buf.WriteString("\n" + name + " Flags:\n") + buf.WriteString(fs.FlagUsages()) + } + + return buf.String() +} + +// renderGroupedFlags groups flags by their "speakeasy:group" annotation, +// rendering each group as a separate section with a header. Flags without +// a group annotation are collected under the fallbackHeader section. +func renderGroupedFlags(flags *pflag.FlagSet, fallbackHeader string) string { + groups := make(map[string]*pflag.FlagSet) + for _, g := range globalFlagGroupOrder { + groups[g] = pflag.NewFlagSet(g, pflag.ContinueOnError) + } + ungrouped := pflag.NewFlagSet("ungrouped", pflag.ContinueOnError) + + flags.VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + if ann, ok := f.Annotations["speakeasy:group"]; ok && len(ann) > 0 { + if fs, exists := groups[ann[0]]; exists { + fs.AddFlag(f) + return + } + } + ungrouped.AddFlag(f) + }) + + var buf strings.Builder + first := true + for _, name := range globalFlagGroupOrder { + fs := groups[name] + if !fs.HasFlags() { + continue + } + if !first { + buf.WriteString("\n") + } + buf.WriteString(name + ":\n") + buf.WriteString(fs.FlagUsages()) + first = false + } + if ungrouped.HasFlags() { + if !first { + buf.WriteString("\n") + } + buf.WriteString(fallbackHeader + ":\n") + buf.WriteString(ungrouped.FlagUsages()) + } + return strings.TrimRight(buf.String(), "\n") +} + +func compactHelpGlobalRequested(cmd *cobra.Command) bool { + if cmd == nil || cmd.Root() == nil { + return false + } + requested, err := cmd.Root().Flags().GetBool("help-global") + return err == nil && requested +} + +func compactMachineInterface(cmd *cobra.Command) string { + if cmd == nil || cmd.Root() == nil { + return "" + } + cmd.InheritedFlags() + has := func(name string) bool { + return cmd.Flags().Lookup(name) != nil || cmd.InheritedFlags().Lookup(name) != nil + } + parts := []string{} + if has("json") { + parts = append(parts, "--json") + } else if has("output-format") { + parts = append(parts, "--output-format json") + } + if has("transform") { + parts = append(parts, "--transform ") + } else if has("jq") { + parts = append(parts, "--jq ") + } + if has("raw") { + parts = append(parts, "--raw") + } + if has("dry-run") { + parts = append(parts, "--dry-run") + } + if has("usage") { + parts = append(parts, "--usage") + } + return strings.Join(parts, " · ") +} + +func compactHelpDefaults(cmd *cobra.Command) string { + if cmd == nil || cmd.Annotations == nil { + return "" + } + return cmd.Annotations["speakeasy_help_defaults"] +} + +func compactHelpFooter(cmd *cobra.Command) string { + if cmd == nil || cmd.Annotations["speakeasy_help_footer"] == "false" { + return "" + } + lines := []string{} + if machine := compactMachineInterface(cmd); machine != "" { + lines = append(lines, "Machine interface: "+machine) + } + lines = append(lines, "Globals (auth, network, output): "+cmd.Root().Name()+" --help-global") + learn := cmd.Annotations["speakeasy_help_learn"] + escalate := cmd.Annotations["speakeasy_help_escalate"] + switch { + case learn != "" && escalate != "": + lines = append(lines, "Learn: "+learn+" · escalate: "+escalate) + case learn != "": + lines = append(lines, "Learn: "+learn) + case escalate != "": + lines = append(lines, "Escalate: "+escalate) + } + return strings.Join(lines, string(rune(10))) +} + +func compactRootHelpFooter(cmd *cobra.Command) string { + if cmd == nil || cmd.Root() == nil { + return "" + } + rootName := cmd.Root().Name() + lines := []string{} + if machine := compactMachineInterface(cmd); machine != "" { + lines = append(lines, "Machine interface: "+machine) + } + lines = append(lines, "Globals (auth, network, output): "+rootName+" --help-global") + lines = append(lines, "Setup: export GEMINI_API_KEY=... or "+rootName+" configure") + return strings.Join(lines, string(rune(10))) +} + +// groupedUsageTemplate returns Cobra's default usage template with the local +// and global Flags sections replaced to use grouped rendering. +// Built at runtime via strings.Replace to avoid template delimiter conflicts +// with the code generator's own template engine. +func groupedUsageTemplate() string { + ob := string([]byte{'{', '{'}) + cb := string([]byte{'}', '}'}) + nl := string(rune(10)) + defaultTmpl := (&cobra.Command{}).UsageTemplate() + + oldLocalCondition := ob + "if .HasAvailableLocalFlags" + cb + oldLocal := oldLocalCondition + nl + nl + "Flags:" + nl + ob + ".LocalFlags.FlagUsages | trimTrailingWhitespaces" + cb + ob + "end" + cb + newLocalCondition := ob + "if and .HasParent .HasAvailableLocalFlags" + cb + newLocal := newLocalCondition + nl + nl + ob + "groupedFlagUsages .LocalFlags | trimTrailingWhitespaces" + cb + ob + "end" + cb + result := strings.Replace(defaultTmpl, oldLocal, newLocal, 1) + + defaults := ob + "with compactHelpDefaults ." + cb + nl + nl + "Defaults: " + ob + "." + cb + ob + "end" + cb + result = strings.Replace(result, newLocalCondition, defaults+newLocalCondition, 1) + + oldGlobal := ob + "if .HasAvailableInheritedFlags" + cb + nl + nl + "Global Flags:" + nl + ob + ".InheritedFlags.FlagUsages | trimTrailingWhitespaces" + cb + ob + "end" + cb + result = strings.Replace(result, oldGlobal, "", 1) + + result = strings.Replace(result, nl+nl+"Examples:"+nl, nl+nl+"Just works:"+nl, 1) + + oldFooter := ob + "if .HasAvailableSubCommands" + cb + nl + nl + "Use \"" + ob + ".CommandPath" + cb + " [command] --help\" for more information about a command." + ob + "end" + cb + rootFooter := ob + "if not .HasParent" + cb + ob + "with compactRootHelpFooter ." + cb + nl + nl + ob + "." + cb + ob + "end" + cb + ob + "end" + cb + commandFooter := ob + "if .HasParent" + cb + ob + "with compactHelpFooter ." + cb + nl + nl + ob + "." + cb + ob + "end" + cb + ob + "end" + cb + result = strings.Replace(result, oldFooter, rootFooter+oldFooter+commandFooter, 1) + + result = strings.TrimRight(result, nl) + nl + nl + clierrors.HelpFooter + nl + + return result +} diff --git a/internal/cli/triggers/delete.go b/internal/cli/triggers/delete.go new file mode 100644 index 0000000..b998232 --- /dev/null +++ b/internal/cli/triggers/delete.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var deleteCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource name of the trigger. [required]"}, +} + +// initDeleteCmd initializes the delete command. +func initDeleteCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete", + Short: "Delete a trigger by ID", + Long: "Deletes a trigger.", + Example: "", + Args: cobra.NoArgs, + RunE: runDeleteCmd, + Annotations: map[string]string{ + "speakeasy_operation": "DeleteTrigger", + }, + } + flagutil.RegisterFlags(cmd, deleteCmdMeta) + if err := flagutil.ValidateMeta[operations.DeleteTriggerRequest](deleteCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for delete: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runDeleteCmd executes the delete command. +func runDeleteCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.DeleteTriggerRequest](cmd, deleteCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Triggers.Delete(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/triggers/get.go b/internal/cli/triggers/get.go new file mode 100644 index 0000000..8159c6f --- /dev/null +++ b/internal/cli/triggers/get.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var getCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource name of the trigger. [required]"}, +} + +// initGetCmd initializes the get command. +func initGetCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Get a trigger by ID", + Long: "Gets details of a single trigger.", + Example: "", + Args: cobra.NoArgs, + RunE: runGetCmd, + Annotations: map[string]string{ + "speakeasy_operation": "GetTrigger", + }, + } + flagutil.RegisterFlags(cmd, getCmdMeta) + if err := flagutil.ValidateMeta[operations.GetTriggerRequest](getCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for get: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runGetCmd executes the get command. +func runGetCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetTriggerRequest](cmd, getCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Triggers.Get(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/triggers/list.go b/internal/cli/triggers/list.go new file mode 100644 index 0000000..90c36ea --- /dev/null +++ b/internal/cli/triggers/list.go @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listCmdMeta = []flagutil.FlagMeta{ + {FlagName: "filter", Shorthand: "f", FieldPath: "Filter", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. Filter expression (e.g., by state)."}, + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. The maximum number of triggers to return per page."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. A page token from a previous ListTriggers call."}, +} + +// initListCmd initializes the list command. +func initListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "List triggers for a project", + Long: "Lists triggers for a project.", + Example: " gemini-api triggers list", + Args: cobra.NoArgs, + RunE: runListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ListTriggers", + }, + } + flagutil.RegisterFlags(cmd, listCmdMeta) + if err := flagutil.ValidateMeta[operations.ListTriggersRequest](listCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list: %w", err) + } + cmd.Flags().BoolP("all", "a", false, "Automatically paginate and fetch all results (streams NDJSON for JSON output)") + cmd.Flags().Int("max-pages", 0, "Maximum number of pages to fetch when using --all (0 = no limit)") + parent.AddCommand(cmd) + return nil +} + +// runListCmd executes the list command. +func runListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + allPages, _ := flagutil.GetBoolFlag(cmd, "all") + maxPages, _ := flagutil.GetIntFlag(cmd, "max-pages") + if maxPages < 0 { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages must be zero or greater")) + } + if flagutil.FlagChanged(cmd, "max-pages") && !allPages { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages requires --all")) + } + req, err := flagutil.BuildRequest[operations.ListTriggersRequest](cmd, listCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if allPages && !client.IsDryRun(cmd) { + res, err := s.Triggers.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.PaginatedResult(cmd, res, "ListTriggersResponse", "", maxPages, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Triggers.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + morePages := output.HasMorePages(res, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + + if err := output.Result(cmd, res); err != nil { + return err + } + if morePages && !client.IsDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), "Hint: more pages available. Use --all to fetch all results, or --page-token for manual pagination.") + } + return nil +} diff --git a/internal/cli/triggers/listexecutions.go b/internal/cli/triggers/listexecutions.go new file mode 100644 index 0000000..b329df2 --- /dev/null +++ b/internal/cli/triggers/listexecutions.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listExecutionsCmdMeta = []flagutil.FlagMeta{ + {FlagName: "trigger-id", Shorthand: "t", FieldPath: "TriggerID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource name of the trigger. [required]"}, + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. The maximum number of executions to return per page."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. A page token from a previous ListTriggerExecutions call."}, +} + +// initListExecutionsCmd initializes the list-executions command. +func initListExecutionsCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list-executions", + Short: "List executions for a trigger", + Long: "Lists executions for a trigger.", + Example: "", + Args: cobra.NoArgs, + RunE: runListExecutionsCmd, + Aliases: []string{"le"}, + Annotations: map[string]string{ + "speakeasy_operation": "ListTriggerExecutions", + }, + } + flagutil.RegisterFlags(cmd, listExecutionsCmdMeta) + if err := flagutil.ValidateMeta[operations.ListTriggerExecutionsRequest](listExecutionsCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list-executions: %w", err) + } + cmd.Flags().BoolP("all", "a", false, "Automatically paginate and fetch all results (streams NDJSON for JSON output)") + cmd.Flags().Int("max-pages", 0, "Maximum number of pages to fetch when using --all (0 = no limit)") + parent.AddCommand(cmd) + return nil +} + +// runListExecutionsCmd executes the list-executions command. +func runListExecutionsCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + allPages, _ := flagutil.GetBoolFlag(cmd, "all") + maxPages, _ := flagutil.GetIntFlag(cmd, "max-pages") + if maxPages < 0 { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages must be zero or greater")) + } + if flagutil.FlagChanged(cmd, "max-pages") && !allPages { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages requires --all")) + } + req, err := flagutil.BuildRequest[operations.ListTriggerExecutionsRequest](cmd, listExecutionsCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if allPages && !client.IsDryRun(cmd) { + res, err := s.Triggers.ListExecutions(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.PaginatedResult(cmd, res, "ListTriggerExecutionsResponse", "", maxPages, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Triggers.ListExecutions(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + morePages := output.HasMorePages(res, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + + if err := output.Result(cmd, res); err != nil { + return err + } + if morePages && !client.IsDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), "Hint: more pages available. Use --all to fetch all results, or --page-token for manual pagination.") + } + return nil +} diff --git a/internal/cli/triggers/root.go b/internal/cli/triggers/root.go new file mode 100644 index 0000000..db361d4 --- /dev/null +++ b/internal/cli/triggers/root.go @@ -0,0 +1,65 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitTriggersRoot(parent *cobra.Command) error { + var TriggersCmd = &cobra.Command{ + Use: "triggers", + Short: "Schedule and manage cron triggers that run managed agents", + Long: "Schedule and manage cron triggers that run managed agents", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initListCmd(TriggersCmd); err != nil { + return err + } + + if err := initDeleteCmd(TriggersCmd); err != nil { + return err + } + + if err := initGetCmd(TriggersCmd); err != nil { + return err + } + + if err := initUpdateCmd(TriggersCmd); err != nil { + return err + } + + if err := initListExecutionsCmd(TriggersCmd); err != nil { + return err + } + + if err := initRunCmd(TriggersCmd); err != nil { + return err + } + + parent.AddCommand(TriggersCmd) + return nil +} diff --git a/internal/cli/triggers/run.go b/internal/cli/triggers/run.go new file mode 100644 index 0000000..c64c65a --- /dev/null +++ b/internal/cli/triggers/run.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var runCmdMeta = []flagutil.FlagMeta{ + {FlagName: "trigger-id", Shorthand: "t", FieldPath: "TriggerID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource name of the trigger. [required]"}, +} + +// initRunCmd initializes the run command. +func initRunCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "run", + Short: "Run a trigger immediately", + Long: "Runs a trigger immediately.", + Example: "", + Args: cobra.NoArgs, + RunE: runRunCmd, + Annotations: map[string]string{ + "speakeasy_operation": "RunTrigger", + }, + } + flagutil.RegisterFlags(cmd, runCmdMeta) + if err := flagutil.ValidateMeta[operations.RunTriggerRequest](runCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for run: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runRunCmd executes the run command. +func runRunCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.RunTriggerRequest](cmd, runCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Triggers.Run(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/triggers/update.go b/internal/cli/triggers/update.go new file mode 100644 index 0000000..74c71ba --- /dev/null +++ b/internal/cli/triggers/update.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var updateCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Resource name of the trigger. [required]"}, + {FlagName: "display-name", FieldPath: "Body.DisplayName", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The display name of the trigger."}, + {FlagName: "status", Shorthand: "s", FieldPath: "Body.Status", Kind: flagutil.FlagKindEnum, Optional: true, EnumValues: []string{"active", "paused", "error"}, Description: "Optional. The status of the trigger. (options: active, paused, error)"}, +} + +// initUpdateCmd initializes the update command. +func initUpdateCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "update", + Short: "Update a trigger by ID", + Long: "Updates a trigger.", + Example: "", + Args: cobra.NoArgs, + RunE: runUpdateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "UpdateTrigger", + }, + } + flagutil.RegisterFlags(cmd, updateCmdMeta) + if err := flagutil.ValidateMeta[operations.UpdateTriggerRequest](updateCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for update: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, updateCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for update: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runUpdateCmd executes the update command. +func runUpdateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "UpdateTrigger") + } + req, err := flagutil.BuildRequest[operations.UpdateTriggerRequest](cmd, updateCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Triggers.Update(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/version.go b/internal/cli/version.go new file mode 100644 index 0000000..61790fb --- /dev/null +++ b/internal/cli/version.go @@ -0,0 +1,73 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +// Version is the current version of the CLI, defaulting to the version from gen.yaml. +// It can be overridden at build time via ldflags targeting the main package, +// which propagates the value here (see cmd/gemini-api/main.go): +// +// go build -ldflags "-X main.version=x.y.z" ./cmd/gemini-api +var Version = "0.6.0" + +// BuildTime is optionally set at build time via ldflags targeting the main package. +var BuildTime string + +// initVersionCmd initializes the version command. +func initVersionCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "version", + Short: "Print the CLI version", + Long: `Print the current version of the gemini-api CLI. + +The version defaults to the SDK version set during generation, but can be +overridden at build time using Go linker flags: + + go build -ldflags "-X main.version=x.y.z -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" ./cmd/gemini-api`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if output.IsMachineMode(cmd) { + info := map[string]any{"name": "gemini-api", "version": Version} + if BuildTime != "" { + info["build_time"] = BuildTime + } + return output.LocalResult(cmd, info) + } + if _, err := fmt.Fprintf(cmd.OutOrStdout(), "gemini-api %s\n", Version); err != nil { + return err + } + if BuildTime != "" { + if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Built: %s\n", BuildTime); err != nil { + return err + } + } + return nil + }, + } + parent.AddCommand(cmd) + return nil +} diff --git a/internal/cli/webhooks/create.go b/internal/cli/webhooks/create.go new file mode 100644 index 0000000..8cb4fc5 --- /dev/null +++ b/internal/cli/webhooks/create.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var createCmdMeta = []flagutil.FlagMeta{ + {FlagName: "name", Shorthand: "n", FieldPath: "Body.Name", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The user-provided name of the webhook."}, + {FlagName: "subscribed-events", Shorthand: "s", FieldPath: "Body.SubscribedEvents", Kind: flagutil.FlagKindStringArray, Required: true, Description: "Required. The events that the webhook is subscribed to.\nAvailable events:\n- batch.succeeded\n- batch.expired\n- batch.failed\n- interaction.requires_action\n- interaction.completed\n- interaction.failed\n- video.generated [required]"}, + {FlagName: "uri", Shorthand: "u", FieldPath: "Body.URI", Kind: flagutil.FlagKindString, Required: true, Description: "Required. The URI to which webhook events will be sent. [required]"}, +} + +// initCreateCmd initializes the create command. +func initCreateCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "create", + Short: "Create a webhook endpoint", + Long: "Creates a new Webhook.", + Example: " gemini-api webhooks create --subscribed-events '[\"batch.expired\"]' --uri https://glaring-thunderbolt.com/", + Args: cobra.NoArgs, + RunE: runCreateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "CreateWebhook", + }, + } + flagutil.RegisterFlags(cmd, createCmdMeta) + if err := flagutil.ValidateMeta[operations.CreateWebhookRequest](createCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for create: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, createCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for create: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runCreateCmd executes the create command. +func runCreateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "CreateWebhook") + } + req, err := flagutil.BuildRequest[operations.CreateWebhookRequest](cmd, createCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.Create(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/webhooks/delete.go b/internal/cli/webhooks/delete.go new file mode 100644 index 0000000..f1aaa09 --- /dev/null +++ b/internal/cli/webhooks/delete.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var deleteCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Required. The ID of the webhook to delete.\nFormat: `{webhook_id}` [required]"}, +} + +// initDeleteCmd initializes the delete command. +func initDeleteCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "delete", + Short: "Delete a webhook by ID", + Long: "Deletes a Webhook.", + Example: "", + Args: cobra.NoArgs, + RunE: runDeleteCmd, + Annotations: map[string]string{ + "speakeasy_operation": "DeleteWebhook", + }, + } + flagutil.RegisterFlags(cmd, deleteCmdMeta) + if err := flagutil.ValidateMeta[operations.DeleteWebhookRequest](deleteCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for delete: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runDeleteCmd executes the delete command. +func runDeleteCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.DeleteWebhookRequest](cmd, deleteCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.Delete(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/webhooks/get.go b/internal/cli/webhooks/get.go new file mode 100644 index 0000000..b35028f --- /dev/null +++ b/internal/cli/webhooks/get.go @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var getCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Required. The ID of the webhook to retrieve. [required]"}, +} + +// initGetCmd initializes the get command. +func initGetCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "get", + Short: "Get a webhook by ID", + Long: "Gets a specific Webhook.", + Example: "", + Args: cobra.NoArgs, + RunE: runGetCmd, + Annotations: map[string]string{ + "speakeasy_operation": "GetWebhook", + }, + } + flagutil.RegisterFlags(cmd, getCmdMeta) + if err := flagutil.ValidateMeta[operations.GetWebhookRequest](getCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for get: %w", err) + } + parent.AddCommand(cmd) + return nil +} + +// runGetCmd executes the get command. +func runGetCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + req, err := flagutil.BuildRequest[operations.GetWebhookRequest](cmd, getCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.Get(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/webhooks/list.go b/internal/cli/webhooks/list.go new file mode 100644 index 0000000..eb0a360 --- /dev/null +++ b/internal/cli/webhooks/list.go @@ -0,0 +1,125 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var listCmdMeta = []flagutil.FlagMeta{ + {FlagName: "page-size", FieldPath: "PageSize", Kind: flagutil.FlagKindInt64, Optional: true, Description: "Optional. The maximum number of webhooks to return. The service may return fewer than\nthis value. If unspecified, at most 50 webhooks will be returned.\nThe maximum value is 1000."}, + {FlagName: "page-token", FieldPath: "PageToken", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. A page token, received from a previous `ListWebhooks` call.\nProvide this to retrieve the subsequent page."}, +} + +// initListCmd initializes the list command. +func initListCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "list", + Short: "List webhook endpoints", + Long: "Lists all Webhooks.", + Example: " gemini-api webhooks list", + Args: cobra.NoArgs, + RunE: runListCmd, + Annotations: map[string]string{ + "speakeasy_operation": "ListWebhooks", + }, + } + flagutil.RegisterFlags(cmd, listCmdMeta) + if err := flagutil.ValidateMeta[operations.ListWebhooksRequest](listCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for list: %w", err) + } + cmd.Flags().BoolP("all", "a", false, "Automatically paginate and fetch all results (streams NDJSON for JSON output)") + cmd.Flags().Int("max-pages", 0, "Maximum number of pages to fetch when using --all (0 = no limit)") + parent.AddCommand(cmd) + return nil +} + +// runListCmd executes the list command. +func runListCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + allPages, _ := flagutil.GetBoolFlag(cmd, "all") + maxPages, _ := flagutil.GetIntFlag(cmd, "max-pages") + if maxPages < 0 { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages must be zero or greater")) + } + if flagutil.FlagChanged(cmd, "max-pages") && !allPages { + return flagutil.WithCLIValidation(fmt.Errorf("--max-pages requires --all")) + } + req, err := flagutil.BuildRequest[operations.ListWebhooksRequest](cmd, listCmdMeta, "", "") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if allPages && !client.IsDryRun(cmd) { + res, err := s.Webhooks.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + return output.PaginatedResult(cmd, res, "WebhookListResponse", "", maxPages, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.List(cmd.Context(), req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + morePages := output.HasMorePages(res, output.PaginationProbe{ + Type: "cursor", + CursorKind: "string", + NextCursor: "$.next_page_token", + NextURL: "", + Results: "", + HasLimit: false, + }) + + if err := output.Result(cmd, res); err != nil { + return err + } + if morePages && !client.IsDryRun(cmd) && !output.IsMachineMode(cmd) { + fmt.Fprintln(cmd.ErrOrStderr(), "Hint: more pages available. Use --all to fetch all results, or --page-token for manual pagination.") + } + return nil +} diff --git a/internal/cli/webhooks/ping.go b/internal/cli/webhooks/ping.go new file mode 100644 index 0000000..b92523c --- /dev/null +++ b/internal/cli/webhooks/ping.go @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var pingCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Required. The ID of the webhook to ping.\nFormat: `{webhook_id}` [required]"}, + {FlagName: "body-param", Shorthand: "b", FieldPath: "Body", Kind: flagutil.FlagKindJSON, Optional: true, Annotations: `request:"mediaType=application/json"`, Description: "The request body."}, +} + +// initPingCmd initializes the ping command. +func initPingCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "ping", + Short: "Send a ping event to a webhook", + Long: "Sends a ping event to a Webhook.", + Example: "", + Args: cobra.NoArgs, + RunE: runPingCmd, + Annotations: map[string]string{ + "speakeasy_operation": "PingWebhook", + }, + } + flagutil.RegisterFlags(cmd, pingCmdMeta) + if err := flagutil.ValidateMeta[operations.PingWebhookRequest](pingCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for ping: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, pingCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for ping: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runPingCmd executes the ping command. +func runPingCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "PingWebhook") + } + req, err := flagutil.BuildRequest[operations.PingWebhookRequest](cmd, pingCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.Ping(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/webhooks/root.go b/internal/cli/webhooks/root.go new file mode 100644 index 0000000..cfc3606 --- /dev/null +++ b/internal/cli/webhooks/root.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +func InitWebhooksRoot(parent *cobra.Command) error { + var WebhooksCmd = &cobra.Command{ + Use: "webhooks", + Short: "Manage webhook endpoints and signing secrets for event delivery", + Long: "Manage webhook endpoints and signing secrets for event delivery", + Args: cobra.NoArgs, + Annotations: map[string]string{"speakeasy_cli_group": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + if err := initListCmd(WebhooksCmd); err != nil { + return err + } + + if err := initCreateCmd(WebhooksCmd); err != nil { + return err + } + + if err := initDeleteCmd(WebhooksCmd); err != nil { + return err + } + + if err := initGetCmd(WebhooksCmd); err != nil { + return err + } + + if err := initUpdateCmd(WebhooksCmd); err != nil { + return err + } + + if err := initPingCmd(WebhooksCmd); err != nil { + return err + } + + if err := initRotateSigningSecretCmd(WebhooksCmd); err != nil { + return err + } + + parent.AddCommand(WebhooksCmd) + return nil +} diff --git a/internal/cli/webhooks/rotatesigningsecret.go b/internal/cli/webhooks/rotatesigningsecret.go new file mode 100644 index 0000000..347c4c5 --- /dev/null +++ b/internal/cli/webhooks/rotatesigningsecret.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var rotateSigningSecretCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Required. The ID of the webhook for which to generate a signing secret.\nFormat: `{webhook_id}` [required]"}, + {FlagName: "revocation-behavior", Shorthand: "r", FieldPath: "Body.RevocationBehavior", Kind: flagutil.FlagKindEnum, Optional: true, EnumValues: []string{"revoke_previous_secrets_after_h24", "revoke_previous_secrets_immediately"}, Description: "Optional. The revocation behavior for previous signing secrets. (options: revoke_previous_secrets_after_h24, revoke_previous_secrets_immediately)"}, +} + +// initRotateSigningSecretCmd initializes the rotate-signing-secret command. +func initRotateSigningSecretCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "rotate-signing-secret", + Short: "Rotate the signing secret for a webhook", + Long: "Generates a new signing secret for a Webhook.", + Example: "", + Args: cobra.NoArgs, + RunE: runRotateSigningSecretCmd, + Aliases: []string{"rss"}, + Annotations: map[string]string{ + "speakeasy_operation": "RotateSigningSecret", + }, + } + flagutil.RegisterFlags(cmd, rotateSigningSecretCmdMeta) + if err := flagutil.ValidateMeta[operations.RotateSigningSecretRequest](rotateSigningSecretCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for rotate-signing-secret: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, rotateSigningSecretCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for rotate-signing-secret: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runRotateSigningSecretCmd executes the rotate-signing-secret command. +func runRotateSigningSecretCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "RotateSigningSecret") + } + req, err := flagutil.BuildRequest[operations.RotateSigningSecretRequest](cmd, rotateSigningSecretCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.RotateSigningSecret(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/webhooks/update.go b/internal/cli/webhooks/update.go new file mode 100644 index 0000000..e80d80a --- /dev/null +++ b/internal/cli/webhooks/update.go @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/client" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +var updateCmdMeta = []flagutil.FlagMeta{ + {FlagName: "id", Shorthand: "i", FieldPath: "ID", Kind: flagutil.FlagKindString, Required: true, Description: "Required. The ID of the webhook to update. [required]"}, + {FlagName: "update-mask", FieldPath: "UpdateMask", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The list of fields to update."}, + {FlagName: "name", Shorthand: "n", FieldPath: "Body.Name", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The user-provided name of the webhook."}, + {FlagName: "state", FieldPath: "Body.State", Kind: flagutil.FlagKindEnum, Optional: true, EnumValues: []string{"enabled", "disabled", "disabled_due_to_failed_deliveries"}, Description: "Optional. The state of the webhook. (options: enabled, disabled, disabled_due_to_failed_deliveries)"}, + {FlagName: "subscribed-events", FieldPath: "Body.SubscribedEvents", Kind: flagutil.FlagKindStringArray, Optional: true, Description: "Optional. The events that the webhook is subscribed to.\nAvailable events:\n- batch.succeeded\n- batch.expired\n- batch.failed\n- interaction.requires_action\n- interaction.completed\n- interaction.failed\n- video.generated"}, + {FlagName: "uri", FieldPath: "Body.URI", Kind: flagutil.FlagKindString, Optional: true, Description: "Optional. The URI to which webhook events will be sent."}, +} + +// initUpdateCmd initializes the update command. +func initUpdateCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "update", + Short: "Update a webhook by ID", + Long: "Updates an existing Webhook.", + Example: "", + Args: cobra.NoArgs, + RunE: runUpdateCmd, + Annotations: map[string]string{ + "speakeasy_operation": "UpdateWebhook", + }, + } + flagutil.RegisterFlags(cmd, updateCmdMeta) + if err := flagutil.ValidateMeta[operations.UpdateWebhookRequest](updateCmdMeta); err != nil { + return fmt.Errorf("invalid metadata for update: %w", err) + } + cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" + if err := flagutil.AnnotateBodyFields(cmd, updateCmdMeta, "Body", "body"); err != nil { + return fmt.Errorf("annotate body fields for update: %w", err) + } + cmd.Flags().Bool("schema", false, "Print the exact JSON Schema of the request body and exit") + _ = flagutil.AnnotatePromptFlag(cmd, "schema", flagutil.PromptFlagSpec{Kind: "bool", DocSurface: true}) + parent.AddCommand(cmd) + return nil +} + +// runUpdateCmd executes the update command. +func runUpdateCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + if requested, _ := cmd.Flags().GetBool("schema"); requested { + return usage.EmitBodySchema(cmd.OutOrStdout(), "UpdateWebhook") + } + req, err := flagutil.BuildRequest[operations.UpdateWebhookRequest](cmd, updateCmdMeta, "Body", "body") + if err != nil { + return flagutil.WithCLIValidation(err) + } + s, err := client.NewClient(cmd) + if err != nil { + return err + } + sdkOpts, err := output.PrepareCallOpts(cmd) + if err != nil { + return err + } + // Dry-run: force skip deserialization so the synthetic empty response + // does not cause parse failures in typed response handling. + if client.IsDryRun(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + res, err := s.Webhooks.Update(cmd.Context(), *req, sdkOpts...) + if err != nil { + return output.Error(cmd, err) + } + + if err := output.Result(cmd, res); err != nil { + return err + } + return nil +} diff --git a/internal/cli/whoami.go b/internal/cli/whoami.go new file mode 100644 index 0000000..1439066 --- /dev/null +++ b/internal/cli/whoami.go @@ -0,0 +1,131 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package cli + +import ( + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +// initWhoamiCmd initializes the whoami command. +func initWhoamiCmd(parent *cobra.Command) error { + var cmd = &cobra.Command{ + Use: "whoami", + Short: "Display current authentication and global parameter configuration", + Long: `Display the currently configured settings and their sources. + +Sources are shown as: + [flag] - Set via command line flag + [env] - Set via environment variable (GEMINI_*) + [keyring] - Set via OS keychain (stored by configure command) + [config] - Set via config file (~/.config/gemini-api/config.yaml) + [unset] - Not configured + +Credential values are masked for security.`, + Args: cobra.NoArgs, + RunE: runWhoamiCmd, + } + parent.AddCommand(cmd) + return nil +} + +// runWhoamiCmd executes the whoami command. +func runWhoamiCmd(cmd *cobra.Command, args []string) error { + if usage.UsageRequested(cmd) { + return usage.EmitSchema(cmd, cmd.OutOrStdout()) + } + + if output.IsMachineMode(cmd) { + info := map[string]any{ + "config_file": config.GetConfigPath(), + "environment_prefix": "GEMINI_", + } + credentials := map[string]any{} + { + value, source := config.ResolveSecurityCredential(cmd, "api-key") + credentials["api-key"] = map[string]any{"source": source, "value": maskSecret(value)} + } + { + value, source := config.ResolveSecurityCredential(cmd, "access-token") + credentials["access-token"] = map[string]any{"source": source, "value": maskSecret(value)} + } + info["credentials"] = credentials + parameters := map[string]any{} + { + value, source := config.ResolveCredential(cmd, "api-version") + parameters["api-version"] = map[string]any{"source": source, "value": value} + } + { + value, source := config.ResolveCredential(cmd, "api-revision") + parameters["api-revision"] = map[string]any{"source": source, "value": value} + } + { + value, source := config.ResolveCredential(cmd, "user-project") + parameters["user-project"] = map[string]any{"source": source, "value": value} + } + info["global_parameters"] = parameters + return output.LocalResult(cmd, info) + } + + out := cmd.OutOrStdout() + fmt.Fprintln(out, "Configuration") + fmt.Fprintln(out, "=============") + fmt.Fprintln(out) + fmt.Fprintf(out, "Config file: %s\n", config.GetConfigPath()) + fmt.Fprintf(out, "Environment prefix: GEMINI_\n") + fmt.Fprintln(out) + fmt.Fprintln(out, "Credentials:") + + // Gemini API key sent as x-goog-api-key. + { + value, source := config.ResolveSecurityCredential(cmd, "api-key") + fmt.Fprintf(out, " --%-25s [%-7s] %s\n", "api-key", source, maskSecret(value)) + } + + // OAuth access token sent as a bearer Authorization header. + { + value, source := config.ResolveSecurityCredential(cmd, "access-token") + fmt.Fprintf(out, " --%-25s [%-7s] %s\n", "access-token", source, maskSecret(value)) + } + + fmt.Fprintln(out) + fmt.Fprintln(out, "Global Parameters:") + + // Which version of the API to use + { + value, source := config.ResolveCredential(cmd, "api-version") + fmt.Fprintf(out, " --%-25s [%-7s] %s\n", "api-version", source, value) + } + + // Interactions API revision to request + { + value, source := config.ResolveCredential(cmd, "api-revision") + fmt.Fprintf(out, " --%-25s [%-7s] %s\n", "api-revision", source, value) + } + + // Quota project header to send with Google GenAI API requests + { + value, source := config.ResolveCredential(cmd, "user-project") + fmt.Fprintf(out, " --%-25s [%-7s] %s\n", "user-project", source, value) + } + + return nil +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..1a4727c --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,244 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package client + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "sync" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk" + "github.com/google-gemini/gemini-api-cli/internal/testclient" + "github.com/spf13/cobra" +) + +// NewClient creates a new SDK client configured from command flags and environment. +// It handles global security, server URL/selection override, global parameters, +// retry configuration, timeout, and test client injection. +// Empty allowedSecurityFields accepts every global security alternative. +func NewClient(cmd *cobra.Command, allowedSecurityFields ...string) (*sdk.GeminiAPI, error) { + var sdkOpts []sdk.SDKOption + sdkOpts = append(sdkOpts, sdk.WithSecurity(buildGlobalSecurity(cmd, allowedSecurityFields))) + if serverURL, _ := flagutil.GetStringFlag(cmd, "server-url"); serverURL != "" { + if err := flagutil.ValidateServerURL(serverURL); err != nil { + return nil, err + } + } + if serverURL, _ := flagutil.GetStringFlag(cmd, "server-url"); serverURL != "" { + sdkOpts = append(sdkOpts, sdk.WithServerURL(serverURL)) + } + + sdkOpts = append(sdkOpts, buildGlobalOptions(cmd)...) + + // Timeout (always available) + if timeoutStr := resolveStringFlag(cmd, "timeout"); timeoutStr != "" { + timeout, err := time.ParseDuration(timeoutStr) + if err != nil { + return nil, flagutil.WithCLIValidation(fmt.Errorf("invalid --timeout value %q: %w", timeoutStr, err)) + } + sdkOpts = append(sdkOpts, sdk.WithTimeout(timeout)) + } // Retry configuration + retryConfig, err := buildRetryConfig(cmd) + if err != nil { + return nil, err + } + if retryConfig != nil { + sdkOpts = append(sdkOpts, sdk.WithRetryConfig(*retryConfig)) + } + + // Diagnostics and test client composition. + // Order: test client (innermost) → diagnostics wrapper (outermost). + var httpClient HTTPClient = &http.Client{Transport: newPhaseBoundedTransport(cmd)} + if testClient := testclient.NewTestHTTPClient(); testClient != nil { + httpClient = testClient + } + httpClient = WrapClientForDiagnostics(cmd, httpClient) + sdkOpts = append(sdkOpts, sdk.WithClient(httpClient)) + return sdk.New(sdkOpts...), nil +} + +func newPhaseBoundedTransport(cmd *cobra.Command) http.RoundTripper { + var phase time.Duration + if s := resolveStringFlag(cmd, "timeout"); s != "" { + d, err := time.ParseDuration(s) + if err != nil || d <= 0 { + return nil + } + phase = d + } + if phase <= 0 { + return nil + } + if cached, ok := phaseBoundedTransports.Load(phase); ok { + return cached.(*http.Transport) + } + transport, ok := http.DefaultTransport.(*http.Transport) + if !ok { + return nil + } + bounded := transport.Clone() + // http.DefaultTransport dials with a 30s timeout and a 10s TLS handshake timeout. + if phase < 30*time.Second { + bounded.DialContext = (&net.Dialer{Timeout: phase, KeepAlive: 30 * time.Second}).DialContext + } + if phase < bounded.TLSHandshakeTimeout { + bounded.TLSHandshakeTimeout = phase + } + bounded.ResponseHeaderTimeout = phase + actual, _ := phaseBoundedTransports.LoadOrStore(phase, bounded) + return actual.(*http.Transport) +} + +var phaseBoundedTransports sync.Map + +// resolveStringFlag reads a string flag with priority: flag > env > config. +func resolveStringFlag(cmd *cobra.Command, name string) string { + if val, changed := flagutil.GetStringFlag(cmd, name); changed && val != "" { + return val + } + return config.GetString(name) +} + +// buildGlobalSecurity reads security credentials with priority: flag > env var > keyring > config. +func buildGlobalSecurity(cmd *cobra.Command, allowedSecurityFields []string) components.Security { + // Resolve request credentials: flag > env var > keyring > config file (keyring skipped for dry-run) + var ( + apiKey string + accessToken string + ) + credentialSources := map[string]string{} + apiKey, credentialSources["api-key"] = config.ResolveRequestSecurityCredential(cmd, "api-key") + accessToken, credentialSources["access-token"] = config.ResolveRequestSecurityCredential(cmd, "access-token") + globalSecurity := components.Security{} + // Rank the alternatives by how explicitly the caller supplied them + // (flag > env > keyring > config; complete before partial at the same + // tier) and send exactly one: an explicit credential picks its scheme + // regardless of the declared order. + credentialCandidates := []config.CredentialCandidate{ + {Field: "APIKey", Complete: apiKey != "", Sources: []string{credentialSources["api-key"]}}, + {Field: "AccessToken", Complete: accessToken != "", Sources: []string{credentialSources["access-token"]}}, + } + switch config.PickCredential(credentialCandidates, allowedSecurityFields) { + case 0: + globalSecurity.APIKey = &apiKey + case 1: + globalSecurity.AccessToken = &accessToken + } + return globalSecurity +} + +// buildGlobalOptions reads global parameter flags and returns SDK options. +// Priority: flag > env var > config file. +func buildGlobalOptions(cmd *cobra.Command) []sdk.SDKOption { + var opts []sdk.SDKOption + if flagutil.FlagChanged(cmd, "api-version") { + val, _ := flagutil.GetStringFlag(cmd, "api-version") + opts = append(opts, sdk.WithAPIVersion(val)) + } else if val := config.GetString("api-version"); val != "" { + opts = append(opts, sdk.WithAPIVersion(val)) + } else { + val, _ := flagutil.GetStringFlag(cmd, "api-version") + opts = append(opts, sdk.WithAPIVersion(val)) + } + if flagutil.FlagChanged(cmd, "api-revision") { + val, _ := flagutil.GetStringFlag(cmd, "api-revision") + opts = append(opts, sdk.WithAPIRevision(val)) + } else if val := config.GetString("api-revision"); val != "" { + opts = append(opts, sdk.WithAPIRevision(val)) + } + if flagutil.FlagChanged(cmd, "user-project") { + val, _ := flagutil.GetStringFlag(cmd, "user-project") + opts = append(opts, sdk.WithUserProject(val)) + } else if val := config.GetString("user-project"); val != "" { + opts = append(opts, sdk.WithUserProject(val)) + } + return opts +} + +// buildRetryConfig reads retry flags and constructs a retry configuration. +// Returns (nil, nil) if no retry flags were set (use spec/SDK defaults). +// Returns non-nil error for invalid flag values (malformed JSON, bad duration). +func buildRetryConfig(cmd *cobra.Command) (*retry.Config, error) { + // --no-retries: highest precedence + if noRetries, changed := flagutil.GetBoolFlag(cmd, "no-retries"); changed { + if noRetries { + return &retry.Config{Strategy: ""}, nil + } + // Explicit --no-retries=false: skip config fallback, proceed to other flags + } else if val := config.GetString("no-retries"); val == "true" { + return &retry.Config{Strategy: ""}, nil + } + + // --retry-config: full JSON override + if configJSON, changed := flagutil.GetStringFlag(cmd, "retry-config"); changed && configJSON != "" { + var cfg retry.Config + if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil { + return nil, flagutil.WithCLIValidation(fmt.Errorf("invalid --retry-config JSON: %w", err)) + } + return &cfg, nil + } + if configJSON := config.GetString("retry-config"); configJSON != "" { + var cfg retry.Config + if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil { + return nil, flagutil.WithCLIValidation(fmt.Errorf("invalid retry-config in config file: %w", err)) + } + return &cfg, nil + } + + // Individual flags: build fresh config with CLI defaults, override specific fields. + // Note: this REPLACES the spec-level retry config entirely (the SDK takes a + // complete retry.Config, not a partial merge). Use --retry-config JSON for + // exact control over all parameters. + var hasOverride bool + retryConfig := retry.Config{ + Strategy: "backoff", + Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 10000, + Exponent: 1.5, + MaxElapsedTime: 30000, + }, + } + + if maxElapsed := resolveStringFlag(cmd, "retry-max-elapsed-time"); maxElapsed != "" { + dur, err := time.ParseDuration(maxElapsed) + if err != nil { + return nil, flagutil.WithCLIValidation(fmt.Errorf("invalid --retry-max-elapsed-time value %q: %w", maxElapsed, err)) + } + retryConfig.Backoff.MaxElapsedTime = int(dur.Milliseconds()) + hasOverride = true + } + if retryConn, changed := flagutil.GetBoolFlag(cmd, "retry-connection-errors"); changed { + retryConfig.RetryConnectionErrors = retryConn + hasOverride = true + } else if val := config.GetString("retry-connection-errors"); val == "true" { + retryConfig.RetryConnectionErrors = true + hasOverride = true + } + + if hasOverride { + return &retryConfig, nil + } + return nil, nil +} diff --git a/internal/client/diagnostics.go b/internal/client/diagnostics.go new file mode 100644 index 0000000..607416d --- /dev/null +++ b/internal/client/diagnostics.go @@ -0,0 +1,722 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package client + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "mime" + "mime/multipart" + "net/http" + "net/url" + "sort" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" +) + +// maxBodyPreview is the maximum number of bytes to show in body previews. +const maxBodyPreview = 4096 + +// maxRedactDepth is the maximum depth for recursive JSON redaction. +const maxRedactDepth = 64 + +// IsDryRun returns true when --dry-run is set on the command. +func IsDryRun(cmd *cobra.Command) bool { + v, _ := flagutil.GetBoolFlag(cmd, "dry-run") + return v +} + +func IsJSONDryRun(cmd *cobra.Command) bool { + if !IsDryRun(cmd) { + return false + } + format := flagutil.ResolveOutputFormat(cmd, config.GetString("output-format"), false) + return format == "json" || flagutil.FlagChanged(cmd, "jq") +} + +// IsDebug returns true when --debug is set and --dry-run is not. +// When both flags are set, dry-run takes precedence. +func IsDebug(cmd *cobra.Command) bool { + if IsDryRun(cmd) { + return false + } + v, _ := flagutil.GetBoolFlag(cmd, "debug") + return v +} + +// sensitiveHeaderKeys lists header key patterns that should be redacted. +// Matching is case-insensitive. Beyond the generic entries, the list includes +// every API-key header name declared by this API's security schemes. +var sensitiveHeaderKeys = []string{ + "authorization", + "proxy-authorization", + "x-api-key", + "api-key", + "x-session-token", + "cookie", + "set-cookie", + "x-goog-api-key", +} + +// sensitiveHeaderSuffixes lists header key suffixes that should be redacted. +var sensitiveHeaderSuffixes = []string{ + "-secret", + "-token", +} + +// sensitiveJSONKeys lists JSON field names to redact, normalized to lowercase without separators. +var sensitiveJSONKeys = map[string]bool{ + "password": true, + "secret": true, + "token": true, + "accesstoken": true, + "refreshtoken": true, + "apikey": true, + "privatekey": true, + "clientsecret": true, +} + +var sensitiveQueryKeys = map[string]bool{ + "apikey": true, + "accesstoken": true, + "token": true, + "key": true, + "clientsecret": true, + "password": true, + "signature": true, +} + +var sensitiveNameSubstrings = []string{ + "key", + "token", + "secret", + "auth", + "session", + "cookie", + "password", + "passwd", + "signature", + "credential", +} + +const redactionDepthMarker = "" + +// isSensitiveHeader returns true if the header key matches a sensitive pattern. +func isSensitiveHeader(key string) bool { + lower := strings.ToLower(key) + for _, s := range sensitiveHeaderKeys { + if lower == s { + return true + } + } + for _, suffix := range sensitiveHeaderSuffixes { + if strings.HasSuffix(lower, suffix) { + return true + } + } + return isSensitiveName(key) +} + +// redactHeaders returns a copy of headers with sensitive values replaced by "[REDACTED]". +func redactHeaders(h http.Header) http.Header { + out := make(http.Header, len(h)) + for k, vals := range h { + if isSensitiveHeader(k) { + out[k] = []string{"[REDACTED]"} + } else { + out[k] = append([]string(nil), vals...) + } + } + return out +} + +func normalizeSensitiveKey(key string) string { + var b strings.Builder + for _, r := range strings.ToLower(key) { + if r != '_' && r != '-' { + b.WriteRune(r) + } + } + return b.String() +} + +func isSensitiveName(key string) bool { + normalized := normalizeSensitiveKey(key) + if normalized == "sig" || sensitiveQueryKeys[normalized] || sensitiveJSONKeys[normalized] { + return true + } + for _, token := range sensitiveNameSubstrings { + if strings.Contains(normalized, token) { + return true + } + } + return false +} + +// redactJSON recursively redacts sensitive keys in a JSON structure. +func redactJSON(v interface{}, depth int) interface{} { + if depth > maxRedactDepth { + return redactionDepthMarker + } + switch val := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(val)) + for k, child := range val { + if sensitiveJSONKeys[normalizeSensitiveKey(k)] { + out[k] = "[REDACTED]" + } else { + out[k] = redactJSON(child, depth+1) + } + } + return out + case []interface{}: + out := make([]interface{}, len(val)) + for i, child := range val { + out[i] = redactJSON(child, depth+1) + } + return out + case string: + return redactBase64String(val) + default: + return v + } +} + +func redactBase64String(value string) string { + if strings.HasPrefix(value, "") { + return value + } + candidate := value + if strings.HasPrefix(strings.ToLower(candidate), "data:") { + comma := strings.IndexByte(candidate, ',') + if comma < 0 || !strings.Contains(strings.ToLower(candidate[:comma]), ";base64") { + return value + } + candidate = candidate[comma+1:] + } + if len(candidate) < 128 || isAllHex(candidate) { + return value + } + + urlAlphabet := strings.ContainsAny(candidate, "-_") + for i, r := range candidate { + valid := r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' + if urlAlphabet { + valid = valid || r == '-' || r == '_' + } else { + valid = valid || r == '+' || r == '/' + } + if r == '=' { + valid = i >= len(candidate)-2 + } + if !valid { + return value + } + } + + padded := strings.HasSuffix(candidate, "=") + if padded && len(candidate)%4 != 0 || !padded && len(candidate)%4 == 1 { + return value + } + var encoding *base64.Encoding + switch { + case urlAlphabet && padded: + encoding = base64.URLEncoding.Strict() + case urlAlphabet: + encoding = base64.RawURLEncoding.Strict() + case padded: + encoding = base64.StdEncoding.Strict() + default: + encoding = base64.RawStdEncoding.Strict() + } + n, err := io.Copy(io.Discard, base64.NewDecoder(encoding, strings.NewReader(candidate))) + if err != nil { + return value + } + return fmt.Sprintf("", n) +} + +func isAllHex(value string) bool { + if value == "" { + return false + } + for _, r := range value { + if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f' || r >= 'A' && r <= 'F') { + return false + } + } + return true +} + +func decodeJSON(body []byte) (interface{}, bool) { + dec := json.NewDecoder(bytes.NewReader(body)) + dec.UseNumber() + var parsed interface{} + if err := dec.Decode(&parsed); err != nil { + return nil, false + } + if err := dec.Decode(&struct{}{}); err != io.EOF { + return nil, false + } + return redactJSON(parsed, 0), true +} + +func isTextMediaType(mediaType string) bool { + return isJSONMediaType(mediaType) || + strings.HasPrefix(mediaType, "text/") || + mediaType == "application/xml" || + strings.HasSuffix(mediaType, "+xml") || + mediaType == "application/x-www-form-urlencoded" +} + +func previewBody(body []byte, contentType string) interface{} { + if len(body) == 0 { + return nil + } + mediaType, params, err := mime.ParseMediaType(contentType) + if err != nil { + mediaType = strings.ToLower(strings.TrimSpace(strings.SplitN(contentType, ";", 2)[0])) + } + if isJSONMediaType(mediaType) || mediaType == "" { + if parsed, ok := decodeJSON(body); ok { + return parsed + } + } + if mediaType == "application/x-www-form-urlencoded" { + return previewForm(body) + } + if strings.HasPrefix(mediaType, "multipart/") { + if boundary := params["boundary"]; boundary != "" { + if summary, ok := previewMultipart(body, boundary); ok { + return summary + } + } + return fmt.Sprintf("", len(body)) + } + if mediaType != "" && !isTextMediaType(mediaType) { + return fmt.Sprintf("", len(body)) + } + return redactBase64String(string(body)) +} + +func previewForm(body []byte) string { + raw := string(body) + if _, err := url.ParseQuery(raw); err != nil { + return fmt.Sprintf("", len(body)) + } + parts := strings.Split(raw, "&") + for i, part := range parts { + key, value, found := strings.Cut(part, "=") + decodedKey, keyErr := url.QueryUnescape(key) + decodedValue, valueErr := url.QueryUnescape(value) + if keyErr != nil || valueErr != nil { + parts[i] = key + "=[REDACTED]" + continue + } + if isSensitiveName(decodedKey) { + parts[i] = key + "=[REDACTED]" + continue + } + redacted := redactBase64String(decodedValue) + if redacted != decodedValue { + parts[i] = key + "=" + redacted + } else if !found { + parts[i] = key + } + } + return strings.Join(parts, "&") +} + +func previewMultipart(body []byte, boundary string) (string, bool) { + r := multipart.NewReader(bytes.NewReader(body), boundary) + var lines []string + for { + part, err := r.NextPart() + if err == io.EOF { + break + } + if err != nil { + return "", false + } + data, err := io.ReadAll(part) + if err != nil { + return "", false + } + name := part.FormName() + if name == "" { + name = "(unnamed)" + } + if part.FileName() != "" { + lines = append(lines, fmt.Sprintf("%s: ", name, len(data))) + continue + } + value := previewBody(data, part.Header.Get("Content-Type")) + if isSensitiveName(name) { + value = "[REDACTED]" + } + lines = append(lines, fmt.Sprintf("%s: %s", name, formatBodyPreview(value, 0))) + } + return strings.Join(lines, string(rune(10))), true +} + +func encodeJSON(value interface{}, indent string) (string, error) { + var out bytes.Buffer + enc := json.NewEncoder(&out) + enc.SetEscapeHTML(false) + if indent != "" { + enc.SetIndent(indent, indent) + } + if err := enc.Encode(value); err != nil { + return "", err + } + return strings.TrimSuffix(out.String(), string(rune(10))), nil +} + +func formatBodyPreview(value interface{}, capBytes int) string { + if value == nil { + return "(empty)" + } + var rendered string + if _, ok := value.(string); ok { + rendered = value.(string) + } else if out, err := encodeJSON(value, " "); err == nil { + rendered = out + } else { + rendered = fmt.Sprint(value) + } + if capBytes > 0 && len(rendered) > capBytes { + return rendered[:capBytes] + "... (truncated)" + } + return rendered +} + +// readAndRestoreBody reads the body from a request or response, then restores +// it so downstream consumers can still read it. +func readAndRestoreBody(body io.ReadCloser) ([]byte, io.ReadCloser, error) { + if body == nil { + return nil, nil, nil + } + data, err := io.ReadAll(body) + body.Close() + return data, io.NopCloser(bytes.NewReader(data)), err +} + +// formatHeaders formats headers for diagnostic output. +func formatHeaders(h http.Header) string { + if len(h) == 0 { + return " (none)\n" + } + var sb strings.Builder + keys := make([]string, 0, len(h)) + for k := range h { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vals := h[k] + sb.WriteString(fmt.Sprintf(" %s: %s\n", k, strings.Join(vals, ", "))) + } + return sb.String() +} + +// redactURL returns the URL string with sensitive query parameters redacted. +func redactURL(u string) string { + parsed, err := url.Parse(u) + if err != nil { + return u + } + // Userinfo may carry an HTTP Basic password. + parsed.User = nil + if parsed.RawQuery == "" { + return parsed.String() + } + parts := strings.Split(parsed.RawQuery, "&") + for i, part := range parts { + key, _, _ := strings.Cut(part, "=") + decoded, err := url.QueryUnescape(key) + if err == nil && isSensitiveName(decoded) { + parts[i] = key + "=[REDACTED]" + } + } + parsed.RawQuery = strings.Join(parts, "&") + return parsed.String() +} + +// DebugClient wraps an HTTP client and logs request/response diagnostics to stderr. +type DebugClient struct { + Inner HTTPClient + Stderr io.Writer +} + +// HTTPClient is the interface that SDK clients implement. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// Do executes the request, logging diagnostics before and after. +func (c *DebugClient) Do(req *http.Request) (*http.Response, error) { + // Log request + fmt.Fprintf(c.Stderr, "[DEBUG] Request: %s %s\n", req.Method, redactURL(req.URL.String())) + fmt.Fprintf(c.Stderr, "[DEBUG] Request Headers:\n%s", formatHeaders(redactHeaders(req.Header))) + if req.Body != nil { + bodyData, restored, readErr := readAndRestoreBody(req.Body) + req.Body = restored + if readErr != nil { + fmt.Fprintf(c.Stderr, "[DEBUG] Request Body Read Error: %v\n", readErr) + } else { + body := previewBody(bodyData, req.Header.Get("Content-Type")) + fmt.Fprintf(c.Stderr, "[DEBUG] Request Body:\n %s\n", formatBodyPreview(body, maxBodyPreview)) + } + } + + // Execute + resp, err := c.Inner.Do(req) + if err != nil { + fmt.Fprintf(c.Stderr, "[DEBUG] Transport Error: %v\n", err) + return nil, err + } + + // Log response + fmt.Fprintf(c.Stderr, "[DEBUG] Response: %d %s\n", resp.StatusCode, resp.Status) + fmt.Fprintf(c.Stderr, "[DEBUG] Response Headers:\n%s", formatHeaders(redactHeaders(resp.Header))) + if resp.Body != nil { + if isStreamingResponse(resp) { + // Reading a streaming body here buffers every event until the server closes the stream. + fmt.Fprintf(c.Stderr, "[DEBUG] Response Body: \n", resp.Header.Get("Content-Type")) + } else { + bodyData, restored, readErr := readAndRestoreBody(resp.Body) + resp.Body = restored + if readErr != nil { + fmt.Fprintf(c.Stderr, "[DEBUG] Response Body Read Error: %v\n", readErr) + } else if len(bodyData) > 0 { + body := previewBody(bodyData, resp.Header.Get("Content-Type")) + fmt.Fprintf(c.Stderr, "[DEBUG] Response Body:\n %s\n", formatBodyPreview(body, maxBodyPreview)) + } + } + } + + return resp, nil +} + +func isStreamingResponse(resp *http.Response) bool { + mediaType := strings.ToLower(strings.TrimSpace(strings.SplitN(resp.Header.Get("Content-Type"), ";", 2)[0])) + if strings.HasPrefix(mediaType, "text/event-stream") { + return true + } + for _, suffix := range []string{"jsonl", "x-ndjson", "json-seq"} { + if strings.HasSuffix(mediaType, "/"+suffix) || strings.HasSuffix(mediaType, "+"+suffix) { + return true + } + } + return false +} + +// DryRunClient intercepts HTTP requests and returns a synthetic response +// without making any network calls. +type DryRunClient struct { + Stderr io.Writer + Stdout io.Writer + JSON bool + Cmd *cobra.Command +} + +type dryRunBody struct{ length int64 } + +// NewDryRunBody returns an empty body that previews as under --dry-run. +func NewDryRunBody(length int64) io.ReadCloser { return &dryRunBody{length: length} } + +func (b *dryRunBody) Read([]byte) (int, error) { return 0, io.EOF } +func (b *dryRunBody) Close() error { return nil } + +type dryRunPreview struct { + DryRun bool `json:"dry_run"` + Request dryRunPreviewRequest `json:"request"` +} + +type dryRunPreviewRequest struct { + Method string `json:"method"` + URL string `json:"url"` + Headers http.Header `json:"headers"` + Body interface{} `json:"body"` +} + +func (c *DryRunClient) Do(req *http.Request) (*http.Response, error) { + flagutil.MarkDryRunRequest(c.Cmd) + var body interface{} + var bodyData []byte + if placeholder, ok := req.Body.(*dryRunBody); ok { + body = fmt.Sprintf("", placeholder.length) + } else if req.Body != nil { + var restored io.ReadCloser + var err error + bodyData, restored, err = readAndRestoreBody(req.Body) + req.Body = restored + if err != nil { + return nil, fmt.Errorf("read request body for dry-run preview: %w", err) + } + body = previewBody(bodyData, req.Header.Get("Content-Type")) + } + requestURL := redactURL(req.URL.String()) + headers := redactHeaders(req.Header) + if c.JSON { + out := c.Stdout + if out == nil { + out = io.Discard + } + enc := json.NewEncoder(out) + enc.SetEscapeHTML(false) + if err := enc.Encode(dryRunPreview{ + DryRun: true, + Request: dryRunPreviewRequest{ + Method: req.Method, URL: requestURL, Headers: headers, Body: body, + }, + }); err != nil { + return nil, fmt.Errorf("write dry-run preview: %w", err) + } + } else { + stderr := c.Stderr + if stderr == nil { + stderr = io.Discard + } + fmt.Fprintf(stderr, "[DRY-RUN] Would send: %s %s\n", req.Method, requestURL) + fmt.Fprintf(stderr, "[DRY-RUN] Headers:\n%s", formatHeaders(headers)) + if req.Body != nil { + fmt.Fprintf(stderr, "[DRY-RUN] Body:\n %s\n", formatBodyPreview(body, 0)) + } + fmt.Fprintf(stderr, "[DRY-RUN] Network call skipped.\n") + } + + contentType, syntheticBody := dryRunResponseShape(req.Header.Get("Accept")) + if isTokenExchange(req, bodyData) { + contentType = "application/json" + syntheticBody = []byte(`{"access_token":"[DRY-RUN]","token_type":"Bearer","expires_in":3600}`) + } + return &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Header: http.Header{"Content-Type": []string{contentType}}, + Body: io.NopCloser(bytes.NewReader(syntheticBody)), + Request: req, + }, nil +} + +func isTokenExchange(req *http.Request, body []byte) bool { + if req == nil || req.URL == nil { + return false + } + if req.Method == http.MethodPost && strings.HasSuffix(strings.TrimSuffix(strings.ToLower(req.URL.Path), "/"), "/token") { + return true + } + mediaType, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")) + switch { + case mediaType == "application/x-www-form-urlencoded": + values, err := url.ParseQuery(string(body)) + return err == nil && values.Has("grant_type") + case isJSONMediaType(mediaType): + dec := json.NewDecoder(bytes.NewReader(body)) + dec.UseNumber() + var value interface{} + return dec.Decode(&value) == nil && containsJSONKey(value, "granttype") + default: + return false + } +} + +func containsJSONKey(value interface{}, normalizedKey string) bool { + switch current := value.(type) { + case map[string]interface{}: + for key, child := range current { + if normalizeSensitiveKey(key) == normalizedKey || containsJSONKey(child, normalizedKey) { + return true + } + } + case []interface{}: + for _, child := range current { + if containsJSONKey(child, normalizedKey) { + return true + } + } + } + return false +} + +func isJSONMediaType(mediaType string) bool { + mediaType = strings.ToLower(mediaType) + return mediaType == "application/json" || strings.HasSuffix(mediaType, "+json") +} + +// Generated response dispatch may match the complete Content-Type value, parameters included. +func dryRunResponseShape(accept string) (string, []byte) { + first := "" + for _, part := range strings.Split(accept, ",") { + segments := strings.Split(part, ";") + baseType := strings.TrimSpace(segments[0]) + if baseType == "" { + continue + } + + params := make([]string, 0, len(segments)-1) + for _, segment := range segments[1:] { + param := strings.TrimSpace(segment) + if param == "" { + continue + } + name := strings.TrimSpace(strings.SplitN(param, "=", 2)[0]) + if strings.EqualFold(name, "q") { + continue + } + params = append(params, param) + } + + acceptedType := baseType + if len(params) > 0 { + acceptedType += "; " + strings.Join(params, "; ") + } + if baseType == "*/*" { + return "application/json", []byte("{}") + } + if isJSONMediaType(baseType) { + return acceptedType, []byte("{}") + } + if first == "" { + first = acceptedType + } + } + if first == "" { + return "application/json", []byte("{}") + } + return first, nil +} + +// WrapClientForDiagnostics wraps an HTTP client based on diagnostics mode. +// Returns the original client if no diagnostics flags are set. +func WrapClientForDiagnostics(cmd *cobra.Command, inner HTTPClient) HTTPClient { + stderr := cmd.ErrOrStderr() + if IsDryRun(cmd) { + return &DryRunClient{Stderr: stderr, Stdout: cmd.OutOrStdout(), JSON: IsJSONDryRun(cmd), Cmd: cmd} + } + if IsDebug(cmd) { + return &DebugClient{Inner: inner, Stderr: stderr} + } + return inner +} diff --git a/internal/clierrors/clierrors.go b/internal/clierrors/clierrors.go new file mode 100644 index 0000000..7649a98 --- /dev/null +++ b/internal/clierrors/clierrors.go @@ -0,0 +1,97 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package clierrors defines the generated CLI's process-exit contract. +package clierrors + +import "errors" + +const ( + ExitOK = 0 + ExitRuntime = 1 + ExitUsage = 2 + ExitAuth = 3 +) + +const HelpFooter = "Exit codes: 0 ok · 1 runtime · 2 usage · 3 authentication/authorization" + +type ExitCoder interface { + ExitCode() int +} + +type CodedError struct { + code int + err error +} + +func (e *CodedError) Error() string { return e.err.Error() } + +func (e *CodedError) Unwrap() error { return e.err } + +func (e *CodedError) ExitCode() int { return e.code } + +func normalizeCode(code int) int { + switch code { + case ExitRuntime, ExitUsage, ExitAuth: + return code + default: + return ExitRuntime + } +} + +func WithExitCode(err error, code int) error { + if err == nil { + return nil + } + code = normalizeCode(code) + if coded, ok := err.(ExitCoder); ok && normalizeCode(coded.ExitCode()) == code { + return err + } + return &CodedError{code: code, err: err} +} + +func ExitCode(err error) int { + if err == nil { + return ExitOK + } + var coded ExitCoder + if errors.As(err, &coded) { + return normalizeCode(coded.ExitCode()) + } + return ExitRuntime +} + +func ErrorTypeExitCode(errorType string) int { + switch errorType { + case "authentication_error", "authorization_error": + return ExitAuth + case "validation_error": + return ExitUsage + default: + return ExitRuntime + } +} + +func CLIReasonExitCode(reason string) int { + switch reason { + case "CLI_AUTHENTICATION": + return ExitAuth + case "CLI_VALIDATION": + return ExitUsage + default: + return ExitRuntime + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..991c92e --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,369 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package config provides configuration management for the CLI. +// It supports loading configuration from: +// - A YAML file at ~/.config//config.yaml +// - Environment variables with a configurable prefix +// - OS keychain (for security credentials, when available) +// Priority: CLI flags > environment variables > OS keychain > config file +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +// ConfigVersion is the current config file schema version. +// Bump this when making breaking changes to the config format. +const ConfigVersion = 1 + +// SecurityConfig holds authentication credentials. +type SecurityConfig struct { + ApiKey string `yaml:"api_key,omitempty"` + AccessToken string `yaml:"access_token,omitempty"` +} + +// GlobalsConfig holds global parameter values. +type GlobalsConfig struct { + ApiVersion string `yaml:"api_version,omitempty"` + ApiRevision string `yaml:"api_revision,omitempty"` + UserProject string `yaml:"user_project,omitempty"` +} + +// Config holds the CLI configuration values. +// Security credentials and other settings are populated from the config file +// and can be overridden by environment variables or CLI flags. +type Config struct { + Version int `yaml:"version,omitempty"` + Security SecurityConfig `yaml:"security,omitempty"` + Globals GlobalsConfig `yaml:"globals,omitempty"` + OutputFormat string `yaml:"output_format,omitempty"` + Timeout string `yaml:"timeout,omitempty"` + NoRetries string `yaml:"no_retries,omitempty"` + RetryMaxElapsedTime string `yaml:"retry_max_elapsed_time,omitempty"` + RetryConnectionErrors string `yaml:"retry_connection_errors,omitempty"` + RetryConfig string `yaml:"retry_config,omitempty"` +} + +var ( + cfg *Config + cfgMu sync.RWMutex + cliName string + envPrefix string + initMu sync.Mutex + initialized bool + initErr error +) + +// Init loads configuration idempotently. +// It should be called during CLI initialization with the CLI name and environment variable prefix. +// Returns nil for missing config file, error for malformed YAML. +// Subsequent calls are no-ops unless Reset is called first. +func Init(name, prefix string) error { + initMu.Lock() + defer initMu.Unlock() + + if initialized { + return initErr + } + + cliName = name + envPrefix = prefix + + loadedCfg := &Config{} + + // Load config file (lowest priority - can be overridden by env vars and flags) + homeDir, err := os.UserHomeDir() + if err == nil { + configPath := filepath.Join(homeDir, ".config", cliName, "config.yaml") + data, err := os.ReadFile(configPath) + if err != nil { + if !os.IsNotExist(err) { + initErr = flagutil.WithCLIValidation(fmt.Errorf("failed to read config file %s: %w", configPath, err)) + } + // No config file is fine - initErr stays nil for IsNotExist + } else { + // Surface YAML parse errors - if user has a config file, they expect it to work + parsedCfg := &Config{} + if err := yaml.Unmarshal(data, parsedCfg); err != nil { + initErr = flagutil.WithCLIValidation(fmt.Errorf("failed to parse config file %s: %w", configPath, err)) + } else { + loadedCfg = parsedCfg + } + } + } + + cfgMu.Lock() + cfg = loadedCfg + cfgMu.Unlock() + + initialized = true + return initErr +} + +// Reset clears all configuration state, allowing Init to be called again. +// This is intended for use in tests to support re-initialization between test cases. +func Reset() { + initMu.Lock() + defer initMu.Unlock() + + cfgMu.Lock() + cfg = nil + cfgMu.Unlock() + + cliName = "" + envPrefix = "" + initErr = nil + initialized = false +} + +// GetString returns a configuration value with priority: env var > config file. +// The key should be the kebab-case flag name (e.g., "api-key", "bearer-token"). +// CLI flags take highest priority and should be checked by the caller before calling this. +func GetString(key string) string { + if val := GetEnvValue(key); val != "" { + return val + } + return GetConfigValue(key) +} + +// GetEnvValue returns the environment variable value for a flag name, or "". +func GetEnvValue(key string) string { + envKey := envPrefix + "_" + strings.ToUpper(strings.ReplaceAll(key, "-", "_")) + return os.Getenv(envKey) +} + +// GetConfigValue returns the config file value for a flag name, or "". +func GetConfigValue(key string) string { + cfgMu.RLock() + defer cfgMu.RUnlock() + + if cfg == nil { + return "" + } + switch key { + case "api-key": + return cfg.Security.ApiKey + case "access-token": + return cfg.Security.AccessToken + case "output-format": + return cfg.OutputFormat + case "timeout": + return cfg.Timeout + case "no-retries": + return cfg.NoRetries + case "retry-max-elapsed-time": + return cfg.RetryMaxElapsedTime + case "retry-connection-errors": + return cfg.RetryConnectionErrors + case "retry-config": + return cfg.RetryConfig + case "api-version": + return cfg.Globals.ApiVersion + case "api-revision": + return cfg.Globals.ApiRevision + case "user-project": + return cfg.Globals.UserProject + } + return "" +} + +// ResolveCredential resolves a credential value using the priority chain: +// flag > env var > config file. Returns the value and its source +// ("flag", "env", "config", or "unset"). +// Used for global parameters. For security credentials, use ResolveSecurityCredential +// which includes the OS keychain tier. +func ResolveCredential(cmd *cobra.Command, flagName string) (value, source string) { + if val, changed := flagutil.GetStringFlag(cmd, flagName); changed && val != "" { + return val, "flag" + } + if val := GetEnvValue(flagName); val != "" { + return val, "env" + } + if val := GetConfigValue(flagName); val != "" { + return val, "config" + } + return "", "unset" +} + +// ResolveSecurityCredential resolves a security credential using the priority chain: +// flag > env var > OS keychain > config file. +// Returns the value and its source ("flag", "env", "keyring", "config", or "unset"). +// This is used for security fields (tokens, API keys, passwords). For global +// parameters, use ResolveCredential which skips the keyring tier. +func ResolveSecurityCredential(cmd *cobra.Command, flagName string) (value, source string) { + if val, changed := flagutil.GetStringFlag(cmd, flagName); changed && val != "" { + return val, "flag" + } + if val := GetEnvValue(flagName); val != "" { + return val, "env" + } + if val := GetKeyringValue(flagName); val != "" { + return val, "keyring" + } + if val := GetConfigValue(flagName); val != "" { + return val, "config" + } + return "", "unset" +} + +// ResolveRequestSecurityCredential skips the OS keychain on --dry-run (keychain reads can prompt). +func ResolveRequestSecurityCredential(cmd *cobra.Command, flagName string) (value, source string) { + if val, changed := flagutil.GetStringFlag(cmd, flagName); changed && val != "" { + return val, "flag" + } + if val := GetEnvValue(flagName); val != "" { + return val, "env" + } + dryRun, _ := flagutil.GetBoolFlag(cmd, "dry-run") + if !dryRun { + if val := GetKeyringValue(flagName); val != "" { + return val, "keyring" + } + } + if val := GetConfigValue(flagName); val != "" { + return val, "config" + } + return "", "unset" +} + +const credentialRankUnset = 4 + +// CredentialSourceRank ranks a credential source; lower is more explicit and unset ranks last. +func CredentialSourceRank(source string) int { + switch source { + case "flag": + return 0 + case "env": + return 1 + case "keyring": + return 2 + case "config": + return 3 + } + return credentialRankUnset +} + +type CredentialCandidate struct { + // Field is the Go field name of the alternative on the Security struct. + Field string + // Complete reports whether every mandatory member of the alternative is set. + Complete bool + // Sources are the sources of the alternative's members ("unset" allowed). + Sources []string +} + +func (c CredentialCandidate) bestSourceRank() int { + best := credentialRankUnset + for _, source := range c.Sources { + if rank := CredentialSourceRank(source); rank < best { + best = rank + } + } + return best +} + +// PickCredential returns the index of the alternative to send, or -1 when none is eligible. +func PickCredential(candidates []CredentialCandidate, allowedFields []string) int { + order := make([]int, 0, len(candidates)) + if len(allowedFields) > 0 { + for _, field := range allowedFields { + for i, candidate := range candidates { + if candidate.Field == field { + order = append(order, i) + } + } + } + } else { + for i := range candidates { + order = append(order, i) + } + } + + best, bestKey := -1, [2]int{} + for _, i := range order { + rank := candidates[i].bestSourceRank() + if rank == credentialRankUnset { + continue + } + key := [2]int{rank, 1} + if candidates[i].Complete { + key[1] = 0 + } + if best == -1 || key[0] < bestKey[0] || (key[0] == bestKey[0] && key[1] < bestKey[1]) { + best, bestKey = i, key + } + } + return best +} + +// GetConfigPath returns the path to the configuration file. +func GetConfigPath() string { + homeDir, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(homeDir, ".config", cliName, "config.yaml") +} + +// GetConfig returns the current configuration. +// Returns nil if Init has not been called. +func GetConfig() *Config { + cfgMu.RLock() + defer cfgMu.RUnlock() + + return cfg +} + +// SaveConfig saves the configuration to the config file. +// Creates the config directory if it doesn't exist. +func SaveConfig(c *Config) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + + configDir := filepath.Join(homeDir, ".config", cliName) + if err := os.MkdirAll(configDir, 0700); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + configPath := filepath.Join(configDir, "config.yaml") + c.Version = ConfigVersion + data, err := yaml.Marshal(c) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(configPath, data, 0600); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + // Update the in-memory config + cfgMu.Lock() + cfg = c + cfgMu.Unlock() + + return nil +} diff --git a/internal/config/keyring.go b/internal/config/keyring.go new file mode 100644 index 0000000..11ca451 --- /dev/null +++ b/internal/config/keyring.go @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package config + +import ( + "errors" + "sync" + + "github.com/zalando/go-keyring" +) + +// KeyringBackend abstracts keyring operations for testability. +type KeyringBackend interface { + Get(service, key string) (string, error) + Set(service, key, value string) error + Delete(service, key string) error +} + +// defaultKeyring delegates to the real go-keyring library. +type defaultKeyring struct{} + +func (d defaultKeyring) Get(service, key string) (string, error) { + return keyring.Get(service, key) +} +func (d defaultKeyring) Set(service, key, value string) error { + return keyring.Set(service, key, value) +} +func (d defaultKeyring) Delete(service, key string) error { + return keyring.Delete(service, key) +} + +var ( + backend KeyringBackend = defaultKeyring{} + keyringAvailable *bool + keyringMu sync.Mutex +) + +// SetKeyringBackend replaces the keyring backend (for testing). +func SetKeyringBackend(b KeyringBackend) { + keyringMu.Lock() + defer keyringMu.Unlock() + backend = b + keyringAvailable = nil // reset cache when backend changes +} + +// isKeyringAvailable tests whether the OS keyring is accessible. +// Result is cached after first call. Uses a read-only probe (Get) to avoid +// side effects — a "not found" error means the backend is present. +func isKeyringAvailable() bool { + keyringMu.Lock() + defer keyringMu.Unlock() + + if keyringAvailable != nil { + return *keyringAvailable + } + + // Probe with a Get on a key that won't exist. + // ErrNotFound means the backend is working; any other error means unavailable. + _, err := backend.Get(cliName+"-probe", "availability-check") + result := err == keyring.ErrNotFound || err == nil + keyringAvailable = &result + return result +} + +// GetKeyringValue retrieves a credential from the OS keychain. +// Returns "" if keyring is unavailable or key is not found. +func GetKeyringValue(key string) string { + if !isKeyringAvailable() { + return "" + } + val, err := backend.Get(cliName, key) + if err != nil { + return "" + } + return val +} + +func GetStoredSecret(key, fallback string) string { + if val := GetKeyringValue(key); val != "" { + return val + } + return fallback +} + +var ErrKeyringUnavailable = errors.New("OS keychain unavailable") + +func StoreSecret(key, value string, fallback *string) error { + if !KeyringAvailable() { + *fallback = value + return ErrKeyringUnavailable + } + if err := SetKeyringValue(key, value); err != nil { + *fallback = value + return err + } + return nil +} + +// SetKeyringValue stores a credential in the OS keychain. +// Returns an error if the keyring is unavailable. +func SetKeyringValue(key, value string) error { + return backend.Set(cliName, key, value) +} + +// DeleteKeyringValue removes a credential from the OS keychain. +func DeleteKeyringValue(key string) error { + return backend.Delete(cliName, key) +} + +// KeyringAvailable returns whether the OS keychain is accessible. +// Useful for configure command to decide where to store secrets. +func KeyringAvailable() bool { + return isKeyringAvailable() +} + +// ResetKeyring clears the cached keyring availability state and restores the +// default backend. Used in tests to re-probe keyring availability. +func ResetKeyring() { + keyringMu.Lock() + defer keyringMu.Unlock() + keyringAvailable = nil + backend = defaultKeyring{} +} diff --git a/internal/explorer/explorer.go b/internal/explorer/explorer.go new file mode 100644 index 0000000..2934578 --- /dev/null +++ b/internal/explorer/explorer.go @@ -0,0 +1,41 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package explorer + +import ( + "strings" + + "github.com/spf13/cobra" + tea "github.com/charmbracelet/bubbletea" +) + +// Run launches the interactive explorer TUI. +// It returns the selected command args (space-separated), or nil if the user quit. +func Run(root *cobra.Command, version string) ([]string, error) { + tree := BuildTree(root) + m := newModel(tree, version) + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) + result, err := p.Run() + if err != nil { + return nil, err + } + final := result.(model) + if final.selected == "" { + return nil, nil + } + return strings.Fields(final.selected), nil +} diff --git a/internal/explorer/styles.go b/internal/explorer/styles.go new file mode 100644 index 0000000..62957da --- /dev/null +++ b/internal/explorer/styles.go @@ -0,0 +1,121 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package explorer + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/lipgloss" +) + +var ( + accentColor = lipgloss.Color("#38BDF8") + dimmedColor = lipgloss.Color("#64748B") + subtleColor = lipgloss.Color("#475569") + + headerTitleStyle = lipgloss.NewStyle(). + Foreground(accentColor). + Bold(true) + + headerDescStyle = lipgloss.NewStyle(). + Foreground(dimmedColor). + Italic(true) + + backStyle = lipgloss.NewStyle(). + Foreground(dimmedColor) + + backSelectedStyle = lipgloss.NewStyle(). + Foreground(accentColor). + Bold(true) + + selectedItemStyle = lipgloss.NewStyle(). + Foreground(accentColor). + Bold(true) + + normalItemStyle = lipgloss.NewStyle() + + groupItemStyle = lipgloss.NewStyle(). + Bold(true) + + dimmedStyle = lipgloss.NewStyle(). + Foreground(dimmedColor) + + detailTitleStyle = lipgloss.NewStyle(). + Foreground(accentColor). + Bold(true) + + detailDescStyle = lipgloss.NewStyle(). + Foreground(dimmedColor). + Italic(true) + + detailSectionStyle = lipgloss.NewStyle(). + Bold(true) + + requiredFlagStyle = lipgloss.NewStyle(). + Foreground(accentColor) + + statusBarStyle = lipgloss.NewStyle(). + BorderTop(true). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(subtleColor). + PaddingLeft(1). + PaddingRight(1) + + breadcrumbStyle = lipgloss.NewStyle(). + Foreground(accentColor). + Bold(true) + + hintStyle = lipgloss.NewStyle(). + Foreground(dimmedColor) + + searchStyle = lipgloss.NewStyle(). + Foreground(accentColor) + + matchHighlightStyle = lipgloss.NewStyle(). + Foreground(accentColor). + Underline(true) +) + +// keyMap defines the key bindings for the explorer. +type keyMap struct { + Up key.Binding + Down key.Binding + Enter key.Binding + Back key.Binding + Search key.Binding + Quit key.Binding +} + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + ), + Enter: key.NewBinding( + key.WithKeys("enter", "right", "l"), + ), + Back: key.NewBinding( + key.WithKeys("esc", "left", "h", "backspace"), + ), + Search: key.NewBinding( + key.WithKeys("/"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "ctrl+c"), + ), +} diff --git a/internal/explorer/tree.go b/internal/explorer/tree.go new file mode 100644 index 0000000..9282627 --- /dev/null +++ b/internal/explorer/tree.go @@ -0,0 +1,262 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package explorer + +import ( + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// CommandNode represents a single item in the explorer tree. +// It is either a group (has children) or a leaf command. +type CommandNode struct { + Name string + FullPath string // e.g., "create-user" or "tag1 auth" + Description string + LongDesc string + Example string + Flags []FlagInfo + Children []CommandNode + IsGroup bool +} + +// FlagInfo describes a single flag for display in the detail pane. +type FlagInfo struct { + Name string + Description string + Type string + Required bool + Default string +} + +// SearchResult represents a match from a recursive tree search. +type SearchResult struct { + Node CommandNode + Breadcrumb string // e.g., "pets > create" + Score int // match quality (higher = better) +} + +// utilityCommands are excluded from the explorer tree. +var utilityCommands = map[string]bool{ + "configure": true, + "whoami": true, + "version": true, + "explore": true, + "help": true, + "completion": true, +} + +// BuildTree walks a cobra.Command tree and produces a CommandNode tree, +// filtering out utility commands. +func BuildTree(root *cobra.Command) CommandNode { + return buildNode(root) +} + +func buildNode(cmd *cobra.Command) CommandNode { + node := CommandNode{ + Name: cmd.Name(), + FullPath: trimRootPath(cmd.CommandPath()), + Description: cmd.Short, + LongDesc: cmd.Long, + Example: cmd.Example, + } + + children := cmd.Commands() + var childNodes []CommandNode + for _, child := range children { + if child.Hidden || utilityCommands[child.Name()] { + continue + } + childNode := buildNode(child) + childNodes = append(childNodes, childNode) + } + + if len(childNodes) > 0 { + // Sort: groups first, then alphabetically within each category + sort.Slice(childNodes, func(i, j int) bool { + if childNodes[i].IsGroup != childNodes[j].IsGroup { + return childNodes[i].IsGroup + } + return childNodes[i].Name < childNodes[j].Name + }) + node.Children = childNodes + node.IsGroup = true + } + + // Extract flags for leaf commands + if !node.IsGroup { + node.Flags = extractFlags(cmd) + } + + return node +} + +func extractFlags(cmd *cobra.Command) []FlagInfo { + var flags []FlagInfo + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + // Skip inherited persistent flags (global flags) + if cmd.InheritedFlags().Lookup(f.Name) != nil { + return + } + fi := FlagInfo{ + Name: f.Name, + Description: f.Usage, + Type: f.Value.Type(), + Default: f.DefValue, + } + // Check for required annotation + if ann, ok := f.Annotations["speakeasy:required"]; ok && len(ann) > 0 && ann[0] == "true" { + fi.Required = true + } + flags = append(flags, fi) + }) + return flags +} + +// trimRootPath removes the root command name prefix from a command path. +// e.g., "cli create-user" -> "create-user", "cli tag1 auth" -> "tag1 auth" +func trimRootPath(path string) string { + parts := strings.SplitN(path, " ", 2) + if len(parts) > 1 { + return parts[1] + } + return path +} + +// SearchTree recursively searches the command tree for nodes matching the query. +// It returns all matching leaf commands and groups, sorted by match quality, +// with breadcrumb paths showing where each result lives in the tree hierarchy. +func SearchTree(node CommandNode, query string) []SearchResult { + query = strings.ToLower(query) + results := make([]SearchResult, 0) + searchRecursive(node, query, "", &results) + + // Sort by score descending (best matches first) + sort.Slice(results, func(i, j int) bool { + return results[i].Score > results[j].Score + }) + + return results +} + +func searchRecursive(node CommandNode, query string, parentPath string, results *[]SearchResult) { + for _, child := range node.Children { + childPath := child.Name + if parentPath != "" { + childPath = parentPath + " > " + child.Name + } + + nameScore := fuzzyScore(strings.ToLower(child.Name), query) + descScore := fuzzyScore(strings.ToLower(child.Description), query) + + // Use the better of the two scores; name matches get a bonus + bestScore := nameScore + if nameScore > 0 { + bestScore += 10 // bonus for matching in name + } + if descScore > bestScore { + bestScore = descScore + } + + if bestScore > 0 { + *results = append(*results, SearchResult{ + Node: child, + Breadcrumb: childPath, + Score: bestScore, + }) + } + + // Recurse into groups regardless of whether the group itself matched + if child.IsGroup { + searchRecursive(child, query, childPath, results) + } + } +} + +// fuzzyScore returns a quality score for how well query matches target as a subsequence. +// Both inputs must be lowercased. Returns 0 if query is not a subsequence of target. +func fuzzyScore(target, query string) int { + if len(query) == 0 { + return 0 + } + if strings.Contains(target, query) { + // Exact substring: highest tier score + score := 100 + len(query)*10 + // Bonus for matching at start or word boundary + idx := strings.Index(target, query) + if idx == 0 { + score += 20 + } else if idx > 0 && isWordBoundary(target[idx-1]) { + score += 15 + } + // Shorter targets = tighter match + score -= len(target) + if score < 1 { + score = 1 + } + return score + } + + // Subsequence matching with scoring + qi := 0 + score := 0 + consecutive := 0 + prevMatchIdx := -2 // track previous match position for consecutive detection + + for i := 0; i < len(target) && qi < len(query); i++ { + if target[i] == query[qi] { + score += 5 // base per-character match + + // Consecutive bonus + if i == prevMatchIdx+1 { + consecutive++ + score += consecutive * 3 + } else { + consecutive = 0 + } + + // Word boundary bonus + if i == 0 || isWordBoundary(target[i-1]) { + score += 8 + } + + prevMatchIdx = i + qi++ + } + } + + if qi < len(query) { + return 0 // not a full subsequence match + } + + // Shorter targets = tighter match + score -= len(target) / 2 + if score < 1 { + score = 1 + } + return score +} + +func isWordBoundary(c byte) bool { + return c == '-' || c == '_' || c == ' ' || c == '.' +} diff --git a/internal/explorer/tui.go b/internal/explorer/tui.go new file mode 100644 index 0000000..a436bf6 --- /dev/null +++ b/internal/explorer/tui.go @@ -0,0 +1,1012 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package explorer + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// navFrame stores state when entering a group, allowing restoration on back. +type navFrame struct { + node CommandNode + cursor int +} + +type model struct { + // Navigation + root CommandNode // root of the full tree, for deep search + stack []navFrame + current CommandNode + cursor int + + // Search + searching bool + filter string + filtered []int // indices into current.Children matching the filter (shallow) + searchResults []SearchResult // deep search results across the whole tree + + // Display + width int + height int + version string + + // Scroll offset for the list panel + scrollOffset int + + // Outcome + selected string + quit bool +} + +func newModel(root CommandNode, version string) model { + return model{ + root: root, + current: root, + version: version, + } +} + +func (m model) Init() tea.Cmd { + return tea.ClearScreen +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil + + case tea.MouseMsg: + if msg.Action == tea.MouseActionRelease && msg.Button == tea.MouseButtonLeft { + return m.handleClick(msg.X, msg.Y) + } + return m, nil + + case tea.KeyMsg: + if m.searching { + return m.updateSearch(msg) + } + return m.updateNav(msg) + } + return m, nil +} + +// handleClick processes a mouse click at the given screen coordinates. +func (m model) handleClick(x, y int) (tea.Model, tea.Cmd) { + // Ignore clicks outside the left (list) panel + leftWidth := m.width * 2 / 5 + if leftWidth < 20 { + leftWidth = 20 + } + if leftWidth > 50 { + leftWidth = 50 + } + rightWidth := m.width - leftWidth - 3 + if rightWidth < 10 { + leftWidth = m.width // single-panel mode + } + if x > leftWidth { + return m, nil + } + + // 3 header rows: title, description, separator + contentRow := y - 3 + if contentRow < 0 { + return m, nil + } + + if m.searching && m.searchResults != nil { + total := len(m.searchResults) + + // Compute the displayed range, mirroring renderList logic + listRows := m.listRowsAvailable(false) + dispEnd := m.scrollOffset + listRows + if dispEnd > total { + dispEnd = total + } + hasOverflow := m.scrollOffset > 0 || dispEnd < total + if hasOverflow && dispEnd-m.scrollOffset > 1 { + dispEnd-- + } + displayedCount := dispEnd - m.scrollOffset + + // Ignore clicks on the scroll indicator row + if hasOverflow && contentRow == displayedCount { + return m, nil + } + + clickedIdx := m.scrollOffset + contentRow + if clickedIdx >= 0 && clickedIdx < dispEnd { + if m.cursor == clickedIdx { + // Double-click effect: select/enter on second click of same item + result := m.searchResults[clickedIdx] + if result.Node.IsGroup { + m.stack = append(m.stack, navFrame{node: m.current, cursor: 0}) + m.current = result.Node + m.cursor = 0 + m.scrollOffset = 0 + m.searching = false + m.filter = "" + m.filtered = nil + m.searchResults = nil + } else { + m.selected = result.Node.FullPath + return m, tea.Quit + } + } else { + m.cursor = clickedIdx + } + } + return m, nil + } + + items := m.visibleItems() + + listRows := m.listRowsAvailable(m.hasBack()) + + // Compute displayed range, mirroring renderList logic + dispEnd := m.scrollOffset + listRows + if dispEnd > len(items) { + dispEnd = len(items) + } + hasOverflow := m.scrollOffset > 0 || dispEnd < len(items) + if hasOverflow && dispEnd-m.scrollOffset > 1 { + dispEnd-- + } + displayedCount := dispEnd - m.scrollOffset + + // Determine what's at this row: items, scroll indicator, or Back button + row := contentRow + if hasOverflow && row == displayedCount { + // Click on scroll indicator — ignore + return m, nil + } + // If scroll indicator is shown, Back button is shifted down by 1 + backRow := displayedCount + if hasOverflow { + backRow = displayedCount + 1 + } + if m.hasBack() && row == backRow { + // Clicked on Back + if m.cursor == len(items) { + m = m.goBack() + } else { + m.cursor = len(items) + } + return m, nil + } + + clickedIdx := m.scrollOffset + row + if clickedIdx >= m.scrollOffset && clickedIdx < dispEnd { + if m.cursor == clickedIdx { + // Second click on same item: enter/select + if clickedIdx < len(items) { + idx := items[clickedIdx] + child := m.current.Children[idx] + if child.IsGroup { + m.stack = append(m.stack, navFrame{node: m.current, cursor: m.cursor}) + m.current = child + m.cursor = 0 + m.scrollOffset = 0 + } else { + m.selected = child.FullPath + return m, tea.Quit + } + } + } else { + m.cursor = clickedIdx + } + } + return m, nil +} + +// hasBack returns true when we're inside a group. +func (m model) hasBack() bool { + return len(m.stack) > 0 +} + +// totalItems returns the number of navigable items (including back entry). +func (m model) totalItems() int { + n := len(m.visibleItems()) + if m.hasBack() { + n++ + } + return n +} + +// isOnBack returns true when the cursor is on the "← Back" entry (last item). +func (m model) isOnBack() bool { + return m.hasBack() && m.cursor == len(m.visibleItems()) +} + +// childIndex returns the real child index for the current cursor. +// Back is at the end so no offset needed for item indices. +func (m model) childIndex(items []int) int { + if m.cursor >= 0 && m.cursor < len(items) { + return items[m.cursor] + } + return 0 +} + +func (m model) goBack() model { + if len(m.stack) > 0 { + frame := m.stack[len(m.stack)-1] + m.stack = m.stack[:len(m.stack)-1] + m.current = frame.node + m.cursor = frame.cursor + m.scrollOffset = 0 + m.ensureVisible(m.visibleItems()) + } + return m +} + +func (m model) updateNav(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + total := m.totalItems() + + switch { + case key.Matches(msg, keys.Quit): + m.quit = true + return m, tea.Quit + + case key.Matches(msg, keys.Up): + if total > 0 { + m.cursor-- + if m.cursor < 0 { + m.cursor = total - 1 + } + m.ensureVisible(m.visibleItems()) + } + + case key.Matches(msg, keys.Down): + if total > 0 { + m.cursor++ + if m.cursor >= total { + m.cursor = 0 + } + m.ensureVisible(m.visibleItems()) + } + + case key.Matches(msg, keys.Enter): + if m.isOnBack() { + m = m.goBack() + return m, nil + } + items := m.visibleItems() + if len(items) == 0 { + break + } + idx := m.childIndex(items) + child := m.current.Children[idx] + if child.IsGroup { + m.stack = append(m.stack, navFrame{node: m.current, cursor: m.cursor}) + m.current = child + m.cursor = 0 + m.scrollOffset = 0 + } else { + m.selected = child.FullPath + return m, tea.Quit + } + + case key.Matches(msg, keys.Back): + m = m.goBack() + + case key.Matches(msg, keys.Search): + m.searching = true + m.filter = "" + m.filtered = nil + m.cursor = 0 + m.scrollOffset = 0 + } + + return m, nil +} + +func (m model) updateSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.Type { + case tea.KeyEscape: + m.searching = false + m.filter = "" + m.filtered = nil + m.searchResults = nil + m.cursor = 0 + m.scrollOffset = 0 + return m, nil + + case tea.KeyEnter: + if len(m.searchResults) > 0 && m.cursor >= 0 && m.cursor < len(m.searchResults) { + result := m.searchResults[m.cursor] + if result.Node.IsGroup { + m.stack = append(m.stack, navFrame{node: m.current, cursor: 0}) + m.current = result.Node + m.cursor = 0 + m.scrollOffset = 0 + } else { + m.selected = result.Node.FullPath + return m, tea.Quit + } + } + m.searching = false + m.filter = "" + m.filtered = nil + m.searchResults = nil + return m, nil + + case tea.KeyBackspace, tea.KeyDelete: + if len(m.filter) > 0 { + m.filter = m.filter[:len(m.filter)-1] + m.applyFilter() + } + return m, nil + + case tea.KeyUp: + total := m.searchTotal() + if total > 0 { + m.cursor-- + if m.cursor < 0 { + m.cursor = total - 1 + } + m.ensureVisible(nil) + } + return m, nil + + case tea.KeyDown: + total := m.searchTotal() + if total > 0 { + m.cursor++ + if m.cursor >= total { + m.cursor = 0 + } + m.ensureVisible(nil) + } + return m, nil + } + + // Normal character input + if msg.Type == tea.KeyRunes { + m.filter += string(msg.Runes) + m.applyFilter() + } + + return m, nil +} + +// searchTotal returns the number of search results. +func (m model) searchTotal() int { + return len(m.searchResults) +} + +func (m *model) applyFilter() { + if m.filter == "" { + m.filtered = nil + m.searchResults = nil + m.cursor = 0 + return + } + // Deep search from root across the entire tree + m.searchResults = SearchTree(m.root, m.filter) + m.filtered = nil + m.cursor = 0 + m.scrollOffset = 0 +} + +// visibleItems returns the indices of currently visible children. +func (m model) visibleItems() []int { + if m.filtered != nil { + return m.filtered + } + items := make([]int, len(m.current.Children)) + for i := range m.current.Children { + items[i] = i + } + return items +} + +// resolveIndex returns the actual child index for the current cursor position. +func (m model) resolveIndex(items []int) int { + if m.cursor >= 0 && m.cursor < len(items) { + return items[m.cursor] + } + return 0 +} + +func (m model) listRowsAvailable(includeBack bool) int { + rows := m.listHeight() + if includeBack { + rows-- + } + if rows < 0 { + rows = 0 + } + return rows +} + +func (m *model) ensureVisible(items []int) { + rows := m.listRowsAvailable(m.hasBack() && !(m.searching && m.searchResults != nil)) + if rows <= 0 { + return + } + + // Account for scroll indicator: when items overflow the viewport, + // renderList reserves one row for the indicator, reducing visible items by 1. + total := len(items) + if m.searching && m.searchResults != nil { + total = len(m.searchResults) + } + visible := rows + if total > rows && rows > 1 { + visible = rows - 1 + } + + if m.cursor < m.scrollOffset { + m.scrollOffset = m.cursor + } + if m.cursor >= m.scrollOffset+visible { + m.scrollOffset = m.cursor - visible + 1 + } +} + +func (m model) listHeight() int { + // Reserve 3 header lines (title + desc + separator) + 2 status bar lines (border + text) + h := m.height - 5 + if h < 1 { + h = 1 + } + return h +} + +// View renders the two-panel layout. +func (m model) View() string { + if m.width == 0 || m.height == 0 { + return "Loading..." + } + + contentHeight := m.listHeight() + + leftWidth := m.width * 2 / 5 + if leftWidth < 20 { + leftWidth = 20 + } + if leftWidth > 50 { + leftWidth = 50 + } + rightWidth := m.width - leftWidth - 3 // 3 for " │ " separator + if rightWidth < 10 { + // Narrow terminal: give all space to the list, skip detail pane + leftWidth = m.width + rightWidth = 0 + } + + // Build each panel's content + leftContent := m.renderList(leftWidth) + + // Header (3 rows: title, description, separator) + rows := make([]string, 0, 3+contentHeight) + + titleLeft := " " + headerTitleStyle.Render(m.current.Name) + titleRight := dimmedStyle.Render("v" + m.version + " ") + titleGap := m.width - lipgloss.Width(titleLeft) - lipgloss.Width(titleRight) + if titleGap < 1 { + titleGap = 1 + } + rows = append(rows, titleLeft+strings.Repeat(" ", titleGap)+titleRight) + + desc := m.current.Description + if desc == "" { + desc = m.current.LongDesc + } + descRow := "" + if desc != "" { + if idx := strings.IndexByte(desc, '\n'); idx >= 0 { + desc = desc[:idx] + } + descRow = " " + headerDescStyle.Render(truncate(desc, m.width-4)) + } + rows = append(rows, padRight(descRow, m.width)) + + // Separator line + rows = append(rows, dimmedStyle.Render(strings.Repeat("─", m.width))) + + if rightWidth > 0 { + // Two-panel mode + rightContent := m.renderDetail(rightWidth) + + leftContent = padToHeight(leftContent, contentHeight) + rightContent = padToHeight(rightContent, contentHeight) + + leftLines := strings.Split(leftContent, "\n") + rightLines := strings.Split(rightContent, "\n") + + for i := 0; i < contentHeight; i++ { + left := "" + if i < len(leftLines) { + left = leftLines[i] + } + right := "" + if i < len(rightLines) { + right = rightLines[i] + } + + left = padRight(left, leftWidth) + right = padRight(right, rightWidth) + + rows = append(rows, left+" │ "+right) + } + } else { + // Single-panel mode (narrow terminal) + leftContent = padToHeight(leftContent, contentHeight) + leftLines := strings.Split(leftContent, "\n") + + for i := 0; i < contentHeight; i++ { + line := "" + if i < len(leftLines) { + line = leftLines[i] + } + rows = append(rows, padRight(line, m.width)) + } + } + + // Status bar + status := m.renderStatus() + + return strings.Join(rows, "\n") + "\n" + status +} + +// padRight pads a string to the given width with spaces, or truncates if too long. +func padRight(s string, width int) string { + w := lipgloss.Width(s) + if w >= width { + return s + } + return s + strings.Repeat(" ", width-w) +} + +// padToHeight ensures content has exactly `height` lines. +func padToHeight(content string, height int) string { + lines := strings.Split(strings.TrimRight(content, "\n"), "\n") + for len(lines) < height { + lines = append(lines, "") + } + if len(lines) > height { + lines = lines[:height] + } + return strings.Join(lines, "\n") +} + +func (m model) renderList(width int) string { + listRows := m.listRowsAvailable(m.hasBack()) + + var b strings.Builder + + maxNameLen := width - 6 // account for " " prefix + padding + if maxNameLen < 10 { + maxNameLen = 10 + } + + // Deep search mode: render flat search results + if m.searching && m.searchResults != nil { + total := len(m.searchResults) + start := m.scrollOffset + end := start + listRows + if end > total { + end = total + } + + hasAbove := start > 0 + hasBelow := end < total + + // Reserve a row for the scroll indicator when items overflow + if (hasAbove || hasBelow) && end-start > 1 { + end-- + hasBelow = end < total + } + + for vi := start; vi < end; vi++ { + result := m.searchResults[vi] + name := result.Node.Name + if result.Node.IsGroup { + name += "/" + } + name = truncate(name, maxNameLen) + + if vi == m.cursor { + b.WriteString(selectedItemStyle.Render("> " + name)) + } else { + // Highlight the matching portion in non-selected items + b.WriteString(" " + highlightMatch(name, m.filter)) + } + b.WriteString("\n") + } + + if hasAbove || hasBelow { + pos := fmt.Sprintf("%d–%d of %d", start+1, end, total) + indicator := " " + if hasAbove { + indicator += "▲ " + } else { + indicator += " " + } + indicator += pos + if hasBelow { + indicator += " ▼" + } + b.WriteString(dimmedStyle.Render(indicator)) + b.WriteString("\n") + } + + if total == 0 { + b.WriteString(dimmedStyle.Render(" No matches")) + } + + return b.String() + } + + // Normal navigation mode: render current level's children + items := m.visibleItems() + + start := m.scrollOffset + end := start + listRows + if end > len(items) { + end = len(items) + } + + hasAbove := start > 0 + hasBelow := end < len(items) + + // Reserve a row for the scroll indicator when items overflow + if (hasAbove || hasBelow) && end-start > 1 { + end-- + hasBelow = end < len(items) + } + + for vi := start; vi < end; vi++ { + idx := items[vi] + child := m.current.Children[idx] + + name := child.Name + if child.IsGroup { + name += "/" + } + name = truncate(name, maxNameLen) + + if vi == m.cursor { + b.WriteString(selectedItemStyle.Render("> " + name)) + } else if child.IsGroup { + b.WriteString(groupItemStyle.Render(" " + name)) + } else { + b.WriteString(normalItemStyle.Render(" " + name)) + } + b.WriteString("\n") + } + + // Scroll position indicator after the list items + if hasAbove || hasBelow { + pos := fmt.Sprintf("%d–%d of %d", start+1, end, len(items)) + indicator := " " + if hasAbove { + indicator += "▲ " + } else { + indicator += " " + } + indicator += pos + if hasBelow { + indicator += " ▼" + } + b.WriteString(dimmedStyle.Render(indicator)) + b.WriteString("\n") + } + + if m.listHeight() == 1 && m.hasBack() { + b.Reset() + } + + // "← Back" as the last item + if m.hasBack() { + if m.isOnBack() { + b.WriteString(backSelectedStyle.Render("> ← Back")) + } else { + b.WriteString(backStyle.Render(" ← Back")) + } + b.WriteString("\n") + } + + if len(items) == 0 && !m.hasBack() { + if m.searching { + b.WriteString(dimmedStyle.Render(" No matches")) + } else { + b.WriteString(dimmedStyle.Render(" No commands")) + } + } + + return b.String() +} + +func (m model) renderDetail(width int) string { + // Deep search mode: show details for the selected search result + if m.searching && m.searchResults != nil { + if len(m.searchResults) == 0 || m.cursor < 0 || m.cursor >= len(m.searchResults) { + return dimmedStyle.Render("Type to search across all commands") + } + result := m.searchResults[m.cursor] + return m.renderNodeDetail(result.Node, result.Breadcrumb, width) + } + + if m.isOnBack() { + parentName := m.breadcrumbName(m.stack[len(m.stack)-1].node.Name, len(m.stack) == 1) + return dimmedStyle.Render("Go back to " + parentName) + } + + items := m.visibleItems() + if len(items) == 0 { + return dimmedStyle.Render("Select a command to see details") + } + + idx := m.childIndex(items) + child := m.current.Children[idx] + + return m.renderNodeDetail(child, "", width) +} + +func (m model) renderNodeDetail(child CommandNode, breadcrumb string, width int) string { + var b strings.Builder + + // Title + b.WriteString(detailTitleStyle.Render(child.Name)) + b.WriteString("\n") + + // Breadcrumb path (shown for search results) + if breadcrumb != "" { + b.WriteString(dimmedStyle.Render(truncate(breadcrumb, width-2))) + b.WriteString("\n") + } + + // Description + desc := child.LongDesc + if desc == "" { + desc = child.Description + } + if desc != "" { + // Word-wrap description + wrapped := wordWrap(desc, width-2) + b.WriteString(detailDescStyle.Render(wrapped)) + b.WriteString("\n\n") + } + + if child.IsGroup { + // Show subcommands + b.WriteString(detailSectionStyle.Render("Commands:")) + b.WriteString("\n") + for _, sub := range child.Children { + suffix := "" + if sub.IsGroup { + suffix = "/" + } + line := fmt.Sprintf(" %-20s %s", sub.Name+suffix, truncate(sub.Description, width-24)) + b.WriteString(dimmedStyle.Render(line)) + b.WriteString("\n") + } + } else { + // Show example + if child.Example != "" { + b.WriteString(detailSectionStyle.Render("Example:")) + b.WriteString("\n") + b.WriteString(dimmedStyle.Render(" " + strings.TrimSpace(child.Example))) + b.WriteString("\n\n") + } + + // Show flags + if len(child.Flags) > 0 { + // Required first + var required, optional []FlagInfo + for _, f := range child.Flags { + if f.Required { + required = append(required, f) + } else { + optional = append(optional, f) + } + } + + if len(required) > 0 { + b.WriteString(detailSectionStyle.Render("Required Flags:")) + b.WriteString("\n") + for _, f := range required { + b.WriteString(requiredFlagStyle.Render(renderFlag(f, width))) + b.WriteString("\n") + } + b.WriteString("\n") + } + + if len(optional) > 0 { + b.WriteString(detailSectionStyle.Render("Optional Flags:")) + b.WriteString("\n") + for _, f := range optional { + b.WriteString(dimmedStyle.Render(renderFlag(f, width))) + b.WriteString("\n") + } + } + } + } + + return b.String() +} + +func (m model) breadcrumbName(name string, isRoot bool) string { + if isRoot { + return "root" + } + return name +} + +func (m model) renderStatus() string { + var breadcrumb strings.Builder + for i, frame := range m.stack { + breadcrumb.WriteString(m.breadcrumbName(frame.node.Name, i == 0)) + breadcrumb.WriteString(" > ") + } + breadcrumb.WriteString(m.breadcrumbName(m.current.Name, len(m.stack) == 0)) + + left := breadcrumbStyle.Render(breadcrumb.String()) + + var right string + if m.searching { + right = searchStyle.Render("/" + m.filter + "█") + } else { + right = hintStyle.Render("↑↓·enter·esc·/·q") + } + + leftW := lipgloss.Width(left) + rightW := lipgloss.Width(right) + available := m.width - 4 // account for padding in statusBarStyle + + gap := available - leftW - rightW + if gap < 1 { + return statusBarStyle.Width(m.width).Render(truncate(breadcrumb.String(), available)) + } + + line := left + strings.Repeat(" ", gap) + right + return statusBarStyle.Width(m.width).Render(line) +} + +// wordWrap wraps text at the given width, respecting word boundaries. +func wordWrap(text string, width int) string { + if width <= 0 { + return text + } + var result strings.Builder + for _, line := range strings.Split(text, "\n") { + words := strings.Fields(line) + lineLen := 0 + for _, word := range words { + if lineLen > 0 && lineLen+1+len(word) > width { + result.WriteString("\n") + lineLen = 0 + } + if lineLen > 0 { + result.WriteString(" ") + lineLen++ + } + result.WriteString(word) + lineLen += len(word) + } + result.WriteString("\n") + } + return strings.TrimRight(result.String(), "\n") +} + +func renderFlag(f FlagInfo, width int) string { + nameCol := fmt.Sprintf(" --%-18s", truncate(f.Name, 18)) + typeCol := fmt.Sprintf("%-8s", f.Type) + descMax := width - len(nameCol) - len(typeCol) - 2 + if descMax < 5 { + descMax = 5 + } + return nameCol + typeCol + truncate(f.Description, descMax) +} + +// highlightMatch renders a string with matched characters highlighted. +// Uses rune-level operations to handle Unicode correctly. +// First tries contiguous substring match; falls back to fuzzy (subsequence) highlighting. +func highlightMatch(s string, query string) string { + if query == "" { + return s + } + runes := []rune(s) + lowerRunes := []rune(strings.ToLower(s)) + queryRunes := []rune(strings.ToLower(query)) + + // Try contiguous substring match first + idx := runeIndex(lowerRunes, queryRunes) + if idx >= 0 { + before := string(runes[:idx]) + match := string(runes[idx : idx+len(queryRunes)]) + after := string(runes[idx+len(queryRunes):]) + return before + matchHighlightStyle.Render(match) + after + } + + // Fall back to fuzzy: highlight each matched character individually + var result strings.Builder + qi := 0 + for i, r := range runes { + if qi < len(queryRunes) && lowerRunes[i] == queryRunes[qi] { + result.WriteString(matchHighlightStyle.Render(string(r))) + qi++ + } else { + result.WriteRune(r) + } + } + if qi < len(queryRunes) { + return s + } + return result.String() +} + +// runeIndex returns the index of the first occurrence of needle in haystack, or -1. +func runeIndex(haystack, needle []rune) int { + if len(needle) == 0 { + return 0 + } + if len(needle) > len(haystack) { + return -1 + } + for i := 0; i <= len(haystack)-len(needle); i++ { + match := true + for j := 0; j < len(needle); j++ { + if haystack[i+j] != needle[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +// truncate shortens s to fit within max display columns, appending "..." if truncated. +// Uses lipgloss.Width for accurate display-width measurement (handles CJK, emoji, etc.) +// and iterates by rune to avoid splitting multi-byte UTF-8 sequences. +func truncate(s string, max int) string { + if max <= 0 { + return "" + } + if lipgloss.Width(s) <= max { + return s + } + ellipsis := "..." + if max <= 3 { + ellipsis = "" + } else { + max -= 3 + } + var result strings.Builder + w := 0 + for _, r := range s { + rw := lipgloss.Width(string(r)) + if w+rw > max { + break + } + result.WriteRune(r) + w += rw + } + return result.String() + ellipsis +} diff --git a/internal/flagutil/async.go b/internal/flagutil/async.go new file mode 100644 index 0000000..e7b17c1 --- /dev/null +++ b/internal/flagutil/async.go @@ -0,0 +1,222 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package flagutil + +import ( + "encoding/json" + "fmt" + "math" + "reflect" + "strconv" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +func SetRequestParameter(request interface{}, operationID, location, name string, value interface{}) error { + if request == nil { + return fmt.Errorf("poll operation %s has a nil request", operationID) + } + v := reflect.ValueOf(request) + if v.Kind() != reflect.Ptr || v.IsNil() { + return fmt.Errorf("poll operation %s request must be a non-nil pointer", operationID) + } + tagKey := location + "Param" + if location == "header" { + tagKey = "header" + } + if setTaggedScalar(v.Elem(), tagKey, name, value, 0) { + return nil + } + return fmt.Errorf("poll operation %s request has no compatible settable scalar field tagged %s parameter %q", operationID, location, name) +} + +func setTaggedScalar(v reflect.Value, tagKey, name string, value interface{}, depth int) bool { + if depth > 4 { + return false + } + for v.IsValid() && v.Kind() == reflect.Ptr { + if v.IsNil() { + if !v.CanSet() { + return false + } + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + if !v.IsValid() || v.Kind() != reflect.Struct { + return false + } + for i := 0; i < v.NumField(); i++ { + fieldType := v.Type().Field(i) + field := v.Field(i) + if taggedParameterName(fieldType.Tag.Get(tagKey)) == name { + return setScalarValue(field, value) + } + if fieldType.Anonymous && setTaggedScalar(field, tagKey, name, value, depth+1) { + return true + } + } + return false +} + +func taggedParameterName(tag string) string { + for _, part := range strings.Split(tag, ",") { + if name, ok := strings.CutPrefix(part, "name="); ok { + return name + } + } + return "" +} + +func setScalarValue(field reflect.Value, value interface{}) bool { + if !field.CanSet() { + return false + } + if field.Kind() == reflect.Ptr { + set := reflect.New(field.Type().Elem()) + if !setScalarValue(set.Elem(), value) { + return false + } + field.Set(set) + return true + } + switch field.Type() { + case reflect.TypeOf(time.Time{}): + return setTimeValue(field, value) + case reflect.TypeOf(types.Date{}): + return setDateValue(field, value) + } + switch field.Kind() { + case reflect.String: + v, ok := value.(string) + if !ok { + return false + } + field.SetString(v) + case reflect.Bool: + v, ok := value.(bool) + if !ok { + return false + } + field.SetBool(v) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v, ok := scalarInt64(value) + if !ok || field.OverflowInt(v) { + return false + } + field.SetInt(v) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v, ok := scalarUint64(value) + if !ok || field.OverflowUint(v) { + return false + } + field.SetUint(v) + case reflect.Float32, reflect.Float64: + v, ok := scalarFloat64(value) + if !ok || field.OverflowFloat(v) { + return false + } + field.SetFloat(v) + default: + return false + } + return true +} + +func setTimeValue(field reflect.Value, value interface{}) bool { + str, ok := value.(string) + if !ok { + return false + } + t, err := time.Parse(time.RFC3339Nano, str) + if err != nil { + if t, err = time.Parse(time.RFC3339, str); err != nil { + return false + } + } + field.Set(reflect.ValueOf(t)) + return true +} + +func setDateValue(field reflect.Value, value interface{}) bool { + str, ok := value.(string) + if !ok { + return false + } + d, err := types.DateFromString(str) + if err != nil { + return false + } + field.Set(reflect.ValueOf(d)) + return true +} + +func scalarInt64(value interface{}) (int64, bool) { + switch v := value.(type) { + case int64: + return v, true + case json.Number: + i, err := strconv.ParseInt(v.String(), 10, 64) + return i, err == nil + case float64: + // float64(math.MaxInt64) rounds up to 2^63, which overflows int64 + if math.IsNaN(v) || math.IsInf(v, 0) || math.Trunc(v) != v || v < math.MinInt64 || v >= math.MaxInt64 { + return 0, false + } + return int64(v), true + } + return 0, false +} + +func scalarUint64(value interface{}) (uint64, bool) { + switch v := value.(type) { + case int64: + if v < 0 { + return 0, false + } + return uint64(v), true + case uint64: + return v, true + case json.Number: + u, err := strconv.ParseUint(v.String(), 10, 64) + return u, err == nil + case float64: + // float64(math.MaxUint64) rounds up to 2^64, which overflows uint64 + if math.IsNaN(v) || math.IsInf(v, 0) || math.Trunc(v) != v || v < 0 || v >= math.MaxUint64 { + return 0, false + } + return uint64(v), true + } + return 0, false +} + +func scalarFloat64(value interface{}) (float64, bool) { + switch v := value.(type) { + case int64: + return float64(v), true + case uint64: + return float64(v), true + case json.Number: + f, err := strconv.ParseFloat(v.String(), 64) + return f, err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) + case float64: + return v, !math.IsNaN(v) && !math.IsInf(v, 0) + } + return 0, false +} diff --git a/internal/flagutil/dispatch.go b/internal/flagutil/dispatch.go new file mode 100644 index 0000000..bc4455a --- /dev/null +++ b/internal/flagutil/dispatch.go @@ -0,0 +1,380 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package flagutil + +import ( + "bytes" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/spf13/cobra" +) + +type DispatchRoute struct { + ID string + Label string + SelectorFlag string + Default bool + PresetJSON string + PresetMerge PresetMerge +} + +type DispatchInput struct { + Name string + BodyKey string + Kind FlagKind + Positional bool + RouteIDs []string + RequiredRouteIDs []string +} + +type DispatchKey struct { + BodyKey string + RouteIDs []string + UnroutedVariants []string +} + +type DispatchTable struct { + Command string + BodyFlag string + Escape string + Routes []DispatchRoute + Inputs []DispatchInput + Keys []DispatchKey +} + +type DispatchError struct { + Message string + Err error +} + +func (e *DispatchError) Error() string { + if e.Message != "" && e.Err != nil { + return fmt.Sprintf("%s: %v", e.Message, e.Err) + } + if e.Message != "" { + return e.Message + } + return e.Err.Error() +} + +func (e *DispatchError) Unwrap() error { return e.Err } +func (*DispatchError) CLIReason() string { return "CLI_VALIDATION" } + +type dispatchEvidence struct { + name string + routes map[string]bool +} + +func PrimeDispatchBody(cmd *cobra.Command, bodyFlag string) (bool, error) { + if FlagChanged(cmd, bodyFlag) { + return true, nil + } + data, err := ReadStdinBody(cmd, bodyFlag) + if err != nil { + return false, dispatchErr("read request body from stdin", err) + } + if len(bytes.TrimSpace(data)) == 0 { + return false, nil + } + if err := cmd.Flags().Set(bodyFlag, string(data)); err != nil { + return false, dispatchErr(fmt.Sprintf("set --%s from stdin", bodyFlag), err) + } + return true, nil +} + +func Select(cmd *cobra.Command, args []string, table DispatchTable) (*DispatchRoute, error) { + if table.BodyFlag == "" { + return nil, dispatchErr("route dispatch requires a body flag", nil) + } + + body, bodySource, bodySupplied, err := dispatchBody(cmd, table.BodyFlag) + if err != nil { + return nil, err + } + + all := make(map[string]bool, len(table.Routes)) + for _, route := range table.Routes { + all[route.ID] = true + } + candidates := cloneDispatchSet(all) + var evidence []dispatchEvidence + addEvidence := func(name string, ids []string) error { + current := dispatchSet(ids) + next := intersectDispatchSets(candidates, current) + if len(next) == 0 { + other := "the supplied request" + for _, prior := range evidence { + if len(intersectDispatchSets(prior.routes, current)) == 0 { + other = prior.name + break + } + } + return dispatchErr(fmt.Sprintf("%s and %s select different request variants; pass exactly one", other, name), nil) + } + candidates = next + evidence = append(evidence, dispatchEvidence{name: name, routes: current}) + return nil + } + + for _, input := range table.Inputs { + changed := input.Positional && len(args) > 0 + if !input.Positional { + changed = FlagChanged(cmd, input.Name) + } + if changed { + if err := addEvidence(dispatchInputSource(input), input.RouteIDs); err != nil { + return nil, err + } + } + } + + keys := make(map[string]DispatchKey, len(table.Keys)) + for _, key := range table.Keys { + keys[key.BodyKey] = key + } + if bodySupplied { + bodyKeys := make([]string, 0, len(body)) + for key := range body { + bodyKeys = append(bodyKeys, key) + } + sort.Strings(bodyKeys) + for _, name := range bodyKeys { + key, known := keys[name] + if !known { + continue // unknown body keys are reported by BuildRequest + } + if len(key.RouteIDs) == 0 && len(key.UnroutedVariants) > 0 { + labels := strings.Join(key.UnroutedVariants, ", ") + variant := "request variant" + if len(key.UnroutedVariants) > 1 { + variant = "request variants" + } + return nil, dispatchErr(fmt.Sprintf("body key %q selects the unrouted %s %s; use %q to send that request", name, labels, variant, table.Escape), nil) + } + if len(key.RouteIDs) > 0 && len(key.RouteIDs) < len(table.Routes) { + if err := addEvidence(fmt.Sprintf("body key %q", name), key.RouteIDs); err != nil { + return nil, err + } + } + } + } + + selected, err := selectDispatchRoute(table.Routes, candidates) + if err != nil { + return nil, err + } + if err := checkDispatchRequired(cmd, args, body, *selected, table.Inputs, len(table.Routes)); err != nil { + return nil, err + } + if body == nil { + body = map[string]json.RawMessage{} + } + for _, input := range table.Inputs { + changed := input.Positional && len(args) > 0 + if !input.Positional { + changed = FlagChanged(cmd, input.Name) + } + if !changed { + continue + } + if _, exists := body[input.BodyKey]; exists { + source := bodySource + if source == "" { + source = "--" + table.BodyFlag + } + return nil, dispatchErr(fmt.Sprintf("key %q is set both by %s and by %s; pass exactly one", input.BodyKey, dispatchInputSource(input), source), nil) + } + value, valueErr := dispatchInputValue(cmd, args, input) + if valueErr != nil { + return nil, dispatchErr(fmt.Sprintf("read %s", dispatchInputSource(input)), valueErr) + } + encoded, marshalErr := json.Marshal(value) + if marshalErr != nil { + return nil, dispatchErr(fmt.Sprintf("encode %s", dispatchInputSource(input)), marshalErr) + } + body[input.BodyKey] = encoded + } + + merge := selected.PresetMerge + if merge.Preset == "" { + merge.Preset = selected.PresetJSON + } + if merge.Command == "" { + merge.Command = table.Command + } + if merge.Escape == "" { + merge.Escape = table.Escape + } + if err := MergePresetObject(body, merge); err != nil { + if _, typed := err.(interface{ CLIReason() string }); typed { + return nil, err + } + return nil, dispatchErr("merge selected route preset", err) + } + encoded, err := json.Marshal(body) + if err != nil { + return nil, dispatchErr("encode dispatched request body", err) + } + if err := cmd.Flags().Set(table.BodyFlag, string(encoded)); err != nil { + return nil, dispatchErr(fmt.Sprintf("set --%s", table.BodyFlag), err) + } + return selected, nil +} + +func dispatchBody(cmd *cobra.Command, bodyFlag string) (map[string]json.RawMessage, string, bool, error) { + var raw []byte + source := "" + if FlagChanged(cmd, bodyFlag) { + value, _ := GetStringFlag(cmd, bodyFlag) + resolved, err := ResolveBodyFlagValue(cmd, bodyFlag, value) + if err != nil { + return nil, "", false, dispatchErr(fmt.Sprintf("resolve --%s", bodyFlag), err) + } + raw = []byte(resolved) + source = "--" + bodyFlag + } else { + data, err := ReadStdinBody(cmd, bodyFlag) + if err != nil { + return nil, "", false, dispatchErr("read request body from stdin", err) + } + raw = data + if len(bytes.TrimSpace(raw)) > 0 { + source = "stdin" + } + } + if len(bytes.TrimSpace(raw)) == 0 { + return nil, source, false, nil + } + var body map[string]json.RawMessage + if err := json.Unmarshal(raw, &body); err != nil || body == nil { + return nil, source, true, dispatchErr("route dispatch requires --body or stdin to contain a JSON object", err) + } + return body, source, true, nil +} + +func selectDispatchRoute(routes []DispatchRoute, candidates map[string]bool) (*DispatchRoute, error) { + for i := range routes { + if candidates[routes[i].ID] && routes[i].Default { + return &routes[i], nil + } + } + if len(candidates) == 1 { + for i := range routes { + if candidates[routes[i].ID] { + return &routes[i], nil + } + } + } + selectors := make([]string, 0, len(candidates)) + for _, route := range routes { + if candidates[route.ID] { + selectors = append(selectors, "--"+route.SelectorFlag) + } + } + return nil, dispatchErr(fmt.Sprintf("pass one of %s", strings.Join(selectors, ", ")), nil) +} + +func checkDispatchRequired(cmd *cobra.Command, args []string, body map[string]json.RawMessage, route DispatchRoute, inputs []DispatchInput, routeCount int) error { + // Pass 0 reports conditionally required inputs, pass 1 universally required ones. + for pass := 0; pass < 2; pass++ { + for _, input := range inputs { + conditional := len(input.RequiredRouteIDs) > 0 && len(input.RequiredRouteIDs) < routeCount + if (pass == 0) != conditional || !dispatchContains(input.RequiredRouteIDs, route.ID) { + continue + } + if _, present := body[input.BodyKey]; present { + continue + } + if input.Positional { + if len(args) == 0 { + return dispatchErr(fmt.Sprintf("required argument <%s> not set for the %s variant", input.Name, route.Label), nil) + } + continue + } + if !FlagChanged(cmd, input.Name) { + return dispatchErr(fmt.Sprintf("required flag --%s not set for the %s variant", input.Name, route.Label), nil) + } + } + } + return nil +} + +func dispatchInputValue(cmd *cobra.Command, args []string, input DispatchInput) (any, error) { + if input.Positional { + return strings.Join(args, " "), nil + } + switch input.Kind { + case FlagKindBool: + return cmd.Flags().GetBool(input.Name) + case FlagKindInt64: + return cmd.Flags().GetInt64(input.Name) + case FlagKindFloat64: + return cmd.Flags().GetFloat64(input.Name) + default: + value, _ := GetStringFlag(cmd, input.Name) + return value, nil + } +} + +func dispatchInputSource(input DispatchInput) string { + if input.Positional { + return fmt.Sprintf("the <%s> argument", input.Name) + } + return "--" + input.Name +} + +func dispatchSet(ids []string) map[string]bool { + out := make(map[string]bool, len(ids)) + for _, id := range ids { + out[id] = true + } + return out +} + +func cloneDispatchSet(in map[string]bool) map[string]bool { + out := make(map[string]bool, len(in)) + for id := range in { + out[id] = true + } + return out +} + +func intersectDispatchSets(left, right map[string]bool) map[string]bool { + out := map[string]bool{} + for id := range left { + if right[id] { + out[id] = true + } + } + return out +} + +func dispatchContains(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} + +func dispatchErr(message string, err error) *DispatchError { + return &DispatchError{Message: message, Err: err} +} diff --git a/internal/flagutil/flags.go b/internal/flagutil/flags.go new file mode 100644 index 0000000..59bf609 --- /dev/null +++ b/internal/flagutil/flags.go @@ -0,0 +1,524 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package flagutil provides utilities for working with Cobra flags, +// particularly for handling persistent flags and the Changed() pattern. +package flagutil + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/url" + "os" + "strings" + "sync/atomic" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// FlagChanged checks if a flag was explicitly set by the user. +// This correctly handles both local flags and inherited persistent flags. +// IMPORTANT: Use this instead of cmd.Flags().Changed() which doesn't +// reliably detect changes on inherited persistent flags. +func FlagChanged(cmd *cobra.Command, name string) bool { + if f := cmd.Flags().Lookup(name); f != nil { + return f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + return f.Changed + } + return false +} + +// GetStringFlag returns the value of a string flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +func GetStringFlag(cmd *cobra.Command, name string) (string, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + return f.Value.String(), f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + return f.Value.String(), f.Changed + } + return "", false +} + +func ResolveOutputFormat(cmd *cobra.Command, configured string, agentMode bool) string { + if val, changed := GetStringFlag(cmd, "output-format"); changed { + return val + } + if configured != "" { + return configured + } + if agentMode { + return "toon" + } + return "pretty" +} + +const dryRunRequestAnnotation = "speakeasy_dry_run_request" + +func MarkDryRunRequest(cmd *cobra.Command) { + if cmd == nil { + return + } + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + cmd.Annotations[dryRunRequestAnnotation] = "true" +} + +func DidDryRunRequest(cmd *cobra.Command) bool { + return cmd != nil && cmd.Annotations[dryRunRequestAnnotation] == "true" +} + +// GetBoolFlag returns the value of a bool flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +func GetBoolFlag(cmd *cobra.Command, name string) (bool, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + val, _ := cmd.Flags().GetBool(name) + return val, f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + // For inherited flags, we need to get the value from the flag set + if boolVal, err := cmd.Flags().GetBool(name); err == nil { + return boolVal, f.Changed + } + // Fall back to parsing the string value + return f.Value.String() == "true", f.Changed + } + return false, false +} + +// GetIntFlag returns the value of an int flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +func GetIntFlag(cmd *cobra.Command, name string) (int, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + val, _ := cmd.Flags().GetInt(name) + return val, f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + if intVal, err := cmd.Flags().GetInt(name); err == nil { + return intVal, f.Changed + } + } + return 0, false +} + +// GetInt64Flag returns the value of an int64 flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +func GetInt64Flag(cmd *cobra.Command, name string) (int64, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + val, _ := cmd.Flags().GetInt64(name) + return val, f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + if int64Val, err := cmd.Flags().GetInt64(name); err == nil { + return int64Val, f.Changed + } + } + return 0, false +} + +// GetFloat64Flag returns the value of a float64 flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +func GetFloat64Flag(cmd *cobra.Command, name string) (float64, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + val, _ := cmd.Flags().GetFloat64(name) + return val, f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + if float64Val, err := cmd.Flags().GetFloat64(name); err == nil { + return float64Val, f.Changed + } + } + return 0, false +} + +// GetStringSliceFlag returns the value of a string slice flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +// StringSlice supports both comma-separated ("a,b") and repeated ("--f a --f b") syntax. +func GetStringSliceFlag(cmd *cobra.Command, name string) ([]string, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + val, _ := cmd.Flags().GetStringSlice(name) + return val, f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + if sliceVal, err := cmd.Flags().GetStringSlice(name); err == nil { + return sliceVal, f.Changed + } + } + return nil, false +} + +// GetStringArrayFlag returns the value of a string array flag and whether it was changed. +// This correctly handles both local flags and inherited persistent flags. +func GetStringArrayFlag(cmd *cobra.Command, name string) ([]string, bool) { + if f := cmd.Flags().Lookup(name); f != nil { + val, _ := cmd.Flags().GetStringArray(name) + return val, f.Changed + } + if f := cmd.InheritedFlags().Lookup(name); f != nil { + if arrayVal, err := cmd.Flags().GetStringArray(name); err == nil { + return arrayVal, f.Changed + } + } + return nil, false +} + +// HasAnyFlagWithPrefix checks if any flag with the given prefix was changed. +// This is useful for detecting if any field in a nested object or union variant was set. +func HasAnyFlagWithPrefix(cmd *cobra.Command, prefix string) bool { + found := false + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if !found && len(f.Name) >= len(prefix) && f.Name[:len(prefix)] == prefix && f.Changed { + found = true + } + }) + if found { + return true + } + cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) { + if !found && len(f.Name) >= len(prefix) && f.Name[:len(prefix)] == prefix && f.Changed { + found = true + } + }) + return found +} + +// AnyFlagsChanged checks if any of the given flags were changed. +// This is useful for checking if any request-shaping flags were set (for stdin mutual exclusion). +func AnyFlagsChanged(cmd *cobra.Command, flagNames []string) bool { + for _, name := range flagNames { + if FlagChanged(cmd, name) { + return true + } + } + return false +} + +func ValidateEnumFlag(cmd *cobra.Command, name string, allowed []string) error { + val, _ := GetStringFlag(cmd, name) + for _, a := range allowed { + if val == a { + return nil + } + } + msg := fmt.Sprintf("invalid value %q for --%s; valid options: %s", val, name, strings.Join(allowed, ", ")) + if suggestion := closestMatch(val, allowed); suggestion != "" { + msg += fmt.Sprintf(" (did you mean %q?)", suggestion) + } + return WithCLIValidation(fmt.Errorf("%s", msg)) +} + +func closestMatch(val string, candidates []string) string { + val = strings.ToLower(val) + if val != "" { + prefix := "" + for _, c := range candidates { + if strings.HasPrefix(strings.ToLower(c), val) { + if prefix != "" { + prefix = "" + break // ambiguous prefix: fall through to edit distance + } + prefix = c + } + } + if prefix != "" { + return prefix + } + } + best, bestDist, ties := "", 3, 0 + for _, c := range candidates { + switch d := editDistance(val, strings.ToLower(c)); { + case d < bestDist: + best, bestDist, ties = c, d, 1 + case d == bestDist: + ties++ + } + } + if ties != 1 { + return "" + } + return best +} + +func SpacedBoolValueHint(cmd *cobra.Command, args []string) string { + if len(args) == 0 { + return "" + } + last := args[len(args)-1] + value := strings.ToLower(last) + if value != "true" && value != "false" { + return "" + } + var changed []string + seen := map[string]bool{} + visit := func(f *pflag.Flag) { + if f.Changed && f.Value.Type() == "bool" && !seen[f.Name] { + seen[f.Name] = true + changed = append(changed, "--"+f.Name) + } + } + cmd.Flags().VisitAll(visit) + cmd.InheritedFlags().VisitAll(visit) + if len(changed) == 0 { + return "" + } + if len(changed) == 1 { + return fmt.Sprintf("Hint: %q was treated as a positional argument, not the value of %s; boolean flags take their value inline: %s=%s", + last, changed[0], changed[0], value) + } + inline := make([]string, len(changed)) + for i, name := range changed { + inline[i] = name + "=" + value + } + return fmt.Sprintf("Hint: %q was treated as a positional argument, not a flag value; boolean flags take their value inline: %s", + last, strings.Join(inline, ", ")) +} + +// DerefOrZero safely dereferences a pointer, returning the zero value if nil. +// Used for optional request bodies with reference types ([]byte, slices, maps) +// where BuildRequestBody returns *T but the SDK method takes T. +func DerefOrZero[T any](p *T) T { + if p == nil { + var zero T + return zero + } + return *p +} + +type cliValidationError struct { + error +} + +func (cliValidationError) CLIReason() string { return "CLI_VALIDATION" } + +func (e cliValidationError) Unwrap() error { return e.error } + +func WithCLIValidation(err error) error { + if err == nil { + return nil + } + return cliValidationError{error: err} +} + +type serverURLValidationError struct { + value string + error +} + +func (serverURLValidationError) CLIReason() string { return "CLI_VALIDATION" } + +func (serverURLValidationError) CLIHints() []string { + return []string{"Pass a valid URL with --server-url (for example, https://api.example.com)"} +} + +func (e serverURLValidationError) Unwrap() error { return e.error } + +func (e serverURLValidationError) Error() string { + return fmt.Sprintf("invalid --server-url %q: %v", e.value, e.error) +} + +func ValidateServerURL(value string) error { + if _, err := url.Parse(value); err != nil { + return serverURLValidationError{value: value, error: err} + } + return nil +} + +// HasStdinInput checks if there is data available on stdin. +// It accepts a cobra.Command to support cmd.SetIn() for testing, +// falling back to os.Stdin.Stat() for production pipe detection. +func HasStdinInput(cmd *cobra.Command) bool { + // If cmd.SetIn() was called, the input was explicitly provided (e.g., by tests) + if cmd.InOrStdin() != os.Stdin { + return true + } + // For real stdin, check if it's a pipe or has data redirected to it + stat, err := os.Stdin.Stat() + if err != nil { + return false + } + return (stat.Mode() & os.ModeCharDevice) == 0 +} + +const stdinReadDeadline = 1 * time.Second + +var stdinDeadlineEnabled atomic.Bool + +func SetStdinReadDeadline(enabled bool) { + stdinDeadlineEnabled.Store(enabled) +} + +const AnnotationWholeBodyFlag = "speakeasy_whole_body_flag" + +type MissingRequiredFlagError struct { + FlagName string + Detail string // optional suffix, e.g. "(or provide via stdin)" +} + +func (e *MissingRequiredFlagError) Error() string { + if e.Detail != "" { + return fmt.Sprintf("missing required flag: --%s %s", e.FlagName, e.Detail) + } + return fmt.Sprintf("missing required flag: --%s", e.FlagName) +} + +func (*MissingRequiredFlagError) CLIReason() string { return "CLI_VALIDATION" } + +type StdinTimeoutError struct { + BodyFlag string +} + +func (e *StdinTimeoutError) Error() string { + return fmt.Sprintf("stdin body did not reach EOF within %s — pipe the complete body promptly, use --%s @- to wait for EOF, or --%s @path to read a file", stdinReadDeadline, e.BodyFlag, e.BodyFlag) +} + +func (*StdinTimeoutError) CLIReason() string { return "CLI_VALIDATION" } + +func ResolveBodyFlagValue(cmd *cobra.Command, flagName, val string) (string, error) { + switch { + case strings.HasPrefix(val, "@@"): + return val[1:], nil + case val == "@-": + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return "", WithCLIValidation(fmt.Errorf("failed to read stdin for --%s @-: %w", flagName, err)) + } + return string(data), nil + case strings.HasPrefix(val, "@") && len(val) > 1: + data, err := os.ReadFile(val[1:]) + if err != nil { + return "", WithCLIValidation(fmt.Errorf("failed to read file for --%s: %w", flagName, err)) + } + return string(data), nil + } + return val, nil +} + +// Valid JSON never begins with '@', so re-resolving the stored value is a no-op. +func ResolveBodyFlag(cmd *cobra.Command, flagName string) error { + value, _ := GetStringFlag(cmd, flagName) + resolved, err := ResolveBodyFlagValue(cmd, flagName, value) + if err != nil { + return err + } + if resolved == value { + return nil + } + return cmd.Flags().Set(flagName, resolved) +} + +// ReadStdinBody returns nil, nil when stdin is a TTY. +func ReadStdinBody(cmd *cobra.Command, bodyFlag string) ([]byte, error) { + in := cmd.InOrStdin() + stdin := os.Stdin // captured once for the reader goroutine + if in != stdin { + return io.ReadAll(in) + } + stat, err := stdin.Stat() + if err != nil || (stat.Mode()&os.ModeCharDevice) != 0 { + return nil, nil + } + if stat.Mode().IsRegular() { + return io.ReadAll(stdin) + } + if !stdinDeadlineEnabled.Load() { + data, err := io.ReadAll(stdin) + if err != nil { + return nil, fmt.Errorf("failed to read stdin: %w", err) + } + return data, nil + } + // os.Stdin.SetReadDeadline is not reliably supported on pipes. + type readResult struct { + data []byte + err error + } + ch := make(chan readResult, 1) + go func() { + data, err := io.ReadAll(stdin) + ch <- readResult{data: data, err: err} + }() + select { + case r := <-ch: + if r.err != nil { + return nil, fmt.Errorf("failed to read stdin: %w", r.err) + } + return r.data, nil + case <-time.After(stdinReadDeadline): + return nil, &StdinTimeoutError{BodyFlag: bodyFlag} + } +} + +func AttachStdinBody(cmd *cobra.Command, bodyFlag string) (bool, error) { + data, err := ReadStdinBody(cmd, bodyFlag) + if err != nil { + return false, err + } + if len(bytes.TrimSpace(data)) == 0 { + return false, nil + } + cmd.SetIn(bytes.NewReader(data)) + return true, nil +} + +// bodyFlagName is "" when the body was attached from stdin by AttachStdinBody. +func MergeInputIntoBody(cmd *cobra.Command, bodyFlagName, key, input string, value any) error { + source, raw := "stdin", []byte(nil) + if bodyFlagName != "" { + s, _ := GetStringFlag(cmd, bodyFlagName) + raw, source = []byte(s), "--"+bodyFlagName + } else { + in := cmd.InOrStdin() + if in == os.Stdin { + return nil + } + data, err := io.ReadAll(in) + if err != nil { + return fmt.Errorf("failed to read stdin: %w", err) + } + raw = data + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(raw, &obj); err != nil || obj == nil { + if err != nil && !json.Valid(raw) { + return fmt.Errorf("failed to parse %s as JSON: %w", source, err) + } + return fmt.Errorf("cannot combine %s with %s: the body must be a JSON object", input, source) + } + if _, present := obj[key]; present { + return fmt.Errorf("key %q is set both by %s and by %s; pass exactly one", key, input, source) + } + encodedValue, err := json.Marshal(value) + if err != nil { + return err + } + obj[key] = encodedValue + merged, err := json.Marshal(obj) + if err != nil { + return err + } + if bodyFlagName != "" { + return cmd.Flags().Set(bodyFlagName, string(merged)) + } + cmd.SetIn(bytes.NewReader(merged)) + return nil +} diff --git a/internal/flagutil/metadata.go b/internal/flagutil/metadata.go new file mode 100644 index 0000000..efab720 --- /dev/null +++ b/internal/flagutil/metadata.go @@ -0,0 +1,2186 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package flagutil + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "path/filepath" + "reflect" + "slices" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" + "github.com/spf13/cobra" +) + +// FlagKind describes what kind of value a flag carries. +type FlagKind int + +const ( + FlagKindString FlagKind = iota // string flag → string field + FlagKindBool // bool flag → bool field + FlagKindInt64 // int64 flag → int64 field + FlagKindFloat64 // float64 flag → float64 field + FlagKindStringArray // string array flag → []string field + FlagKindDateTime // string flag → validated as RFC3339, stored as time.Time or string + FlagKindDate // string flag → validated as date, stored as types.Date or string + FlagKindEnum // string flag → named enum type (reflect.Convert) + FlagKindIntEnum // string flag → parse int → named int enum type + FlagKindJSON // string flag → unmarshal into struct + FlagKindUnion // union field — handled via UnionMeta + FlagKindFile // string flag (file path) → read file → populate Content/FileName struct + FlagKindFileArray // string flag (comma-separated file paths) → read files → populate []FileStruct + FlagKindBytes // string flag → "file:" reads file, "b64:" base64-decodes, else raw UTF-8 bytes → []byte field +) + +// UnionMeta describes a union field's structure for metadata-driven registration and parsing. +type UnionMeta struct { + Discriminated bool // true for discriminated, false for non-discriminated + DiscriminatorKey string // Go field name for discriminator on variant structs (e.g. "Type") + Optional bool // true if the union field itself is *T (pointer) + TypeDescription string // help text for top-level JSON flag + Variants []UnionVariantMeta // variant definitions (empty for non-discriminated) + + VariantKeys []string // distinguishing selector keys across variants (e.g. ["card","bank_transfer"]) + DefaultJSON string // JSON object merged when no VariantKeys key is present +} + +func applyUnionDefaults(body []byte, u *UnionMeta) []byte { + if u == nil || u.DefaultJSON == "" || len(u.VariantKeys) == 0 { + return body + } + // json.RawMessage round-trips large integers and precise decimals byte-for-byte + var obj map[string]json.RawMessage + if err := json.Unmarshal(body, &obj); err != nil || obj == nil { + return body + } + for _, k := range u.VariantKeys { + if _, ok := obj[k]; ok { + return body + } + } + var defs map[string]json.RawMessage + if err := json.Unmarshal([]byte(u.DefaultJSON), &defs); err != nil { + return body + } + for k, v := range defs { + if _, ok := obj[k]; !ok { + obj[k] = v + } + } + out, err := json.Marshal(obj) + if err != nil { + return body + } + return out +} + +func applyNestedUnionDefaults(body []byte, meta []FlagMeta, bodyFieldPath string, bodyType reflect.Type) []byte { + for _, m := range meta { + if m.Kind != FlagKindUnion || m.Union == nil || m.Union.DefaultJSON == "" || len(m.Union.VariantKeys) == 0 || + !isBodyFieldPath(m.FieldPath, bodyFieldPath) || m.FieldPath == bodyFieldPath { + continue + } + rel := m.FieldPath + if bodyFieldPath != "" { + rel = strings.TrimPrefix(rel, bodyFieldPath+".") + } + keys, ok := jsonKeyPathForFields(bodyType, strings.Split(rel, ".")) + if !ok { + continue + } + body = mergeUnionDefaultAtPath(body, keys, m.Union) + } + return body +} + +func jsonKeyPathForFields(t reflect.Type, fields []string) ([]string, bool) { + keys := make([]string, 0, len(fields)) + for _, name := range fields { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, false + } + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + key := strings.Split(field.Tag.Get("json"), ",")[0] + if key == "" || key == "-" { + return nil, false + } + keys = append(keys, key) + t = field.Type + } + return keys, true +} + +func mergeUnionDefaultAtPath(body []byte, keys []string, u *UnionMeta) []byte { + if len(keys) == 0 { + return applyUnionDefaults(body, u) + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(body, &obj); err != nil || obj == nil { + return body + } + child, ok := obj[keys[0]] + if !ok { + return body + } + merged := mergeUnionDefaultAtPath(child, keys[1:], u) + if bytes.Equal(merged, child) { + return body + } + obj[keys[0]] = merged + out, err := json.Marshal(obj) + if err != nil { + return body + } + return out +} + +func bodyUnionMeta(meta []FlagMeta, bodyFieldPath string) *UnionMeta { + for _, m := range meta { + if m.Kind == FlagKindUnion && m.Union != nil && m.FieldPath == bodyFieldPath { + return m.Union + } + } + return nil +} + +// UnionVariantMeta describes a single variant within a discriminated union. +type UnionVariantMeta struct { + DiscriminatorValue string // discriminator value, e.g. "circle" + FlagName string // full flag name, e.g. "shape-request.shape.circle" + FieldName string // Go field name on union struct, e.g. "Circle" + CanExpand bool // whether variant has expanded field flags + Description string // help text for variant JSON flag + Fields []FlagMeta // variant leaf fields (FieldPath relative to variant struct) +} + +// FlagMeta describes a single CLI flag and how it maps to a request struct field. +// This is the single source of truth — used for both Cobra flag registration (RegisterFlags) +// and request building (BuildRequest). This code is designed for CLI startup, not hot paths. +type FlagMeta struct { + // Flag identity + FlagName string // Cobra flag name, e.g. "request-body.emoji" + Shorthand string // Single-letter shorthand for Cobra (e.g. "s" for -s), empty if none + FieldPath string // Dot-delimited Go struct path, e.g. "RequestBody.Emoji" + Kind FlagKind // What type of flag this is + + // Flag behavior + Optional bool // true if Go field is a pointer type + Required bool // true if user must provide this flag + HasDefault bool // true if optional+has-default (apply cobra default when flag unchanged and no body/stdin) + + // Validation + EnumValues []string // valid values for enum validation; nil if not enum + MinLength int64 // schema minLength for string flags (0 = unconstrained) + HasMinimum bool // schema minimum declared for numeric flags + Minimum float64 // schema minimum (valid when HasMinimum) + HasMaximum bool // schema maximum declared for numeric flags + Maximum float64 // schema maximum (valid when HasMaximum) + + // JSON unmarshal + Annotations string // struct tag for JSON unmarshal, e.g. `request:"mediaType=application/json"` + + // Union support + Union *UnionMeta // non-nil when Kind == FlagKindUnion + + // Registration (used by RegisterFlags) + Description string // help text for Cobra + DefaultStr string // default for String/Enum/DateTime/JSON flags + DefaultBool bool // default for Bool flags + DefaultInt int64 // default for Int64 flags + DefaultFloat float64 // default for Float64 flags + + // Display grouping (used by help template) + Group string // optional group label for organizing flags in --help output + + // Value resolution (used by interactive prompting to skip already-resolved flags) + EnvVar string // environment variable name, e.g. "MYAPP_API_KEY"; empty if none + ConfigKey string // config file key, e.g. "api_key"; empty if none +} + +const ( + AnnotationRequired = "speakeasy:required" + AnnotationEnv = "speakeasy:env" + AnnotationConfig = "speakeasy:config" + AnnotationDefaultResolves = "speakeasy:default-resolves" + AnnotationBodyField = "speakeasy:body-field" + AnnotationBodyFlag = "speakeasy:body-flag" + AnnotationDocSurface = "speakeasy:doc-surface" + AnnotationPrompt = "speakeasy:prompt" + AnnotationPromptDirect = "speakeasy:prompt-direct" + AnnotationPromptLabel = "speakeasy:prompt-label" + AnnotationPromptKind = "speakeasy:prompt-kind" + AnnotationPromptValues = "speakeasy:prompt-values" + AnnotationPromptOrder = "speakeasy:prompt-order" + AnnotationUnionMember = "speakeasy:union-member" + AnnotationOpDeclaredInput = "speakeasy:op-declared-input" +) + +type PromptFlagSpec struct { + Required bool + PromptOptional bool + PromptDirect bool + Label string + Kind string + Values []string + Order int + EnvVar string + ConfigKey string + DefaultResolves bool + BodySources []string + BodyFlag bool + DocSurface bool +} + +func AnnotatePromptFlag(cmd *cobra.Command, name string, spec PromptFlagSpec) error { + f := cmd.Flags().Lookup(name) + if f == nil { + f = cmd.InheritedFlags().Lookup(name) + } + if f == nil { + return fmt.Errorf("cannot annotate unknown flag --%s", name) + } + set := func(key string, values []string) { + if len(values) == 0 { + delete(f.Annotations, key) + return + } + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + f.Annotations[key] = values + } + truth := func(v bool) []string { + if v { + return []string{"true"} + } + return nil + } + + set(AnnotationRequired, truth(spec.Required)) + set(AnnotationPrompt, truth(spec.PromptOptional)) + set(AnnotationPromptDirect, truth(spec.PromptDirect)) + if spec.Label != "" { + set(AnnotationPromptLabel, []string{spec.Label}) + } + if spec.Kind != "" { + set(AnnotationPromptKind, []string{spec.Kind}) + } + set(AnnotationPromptValues, spec.Values) + set(AnnotationPromptOrder, []string{strconv.Itoa(spec.Order)}) + if spec.EnvVar != "" { + set(AnnotationEnv, []string{spec.EnvVar}) + } + if spec.ConfigKey != "" { + set(AnnotationConfig, []string{spec.ConfigKey}) + } + set(AnnotationDefaultResolves, truth(spec.DefaultResolves)) + set(AnnotationBodyField, spec.BodySources) + set(AnnotationBodyFlag, truth(spec.BodyFlag)) + set(AnnotationDocSurface, truth(spec.DocSurface)) + return nil +} + +func OverridePromptRequirement(cmd *cobra.Command, name string, required, promptOptional bool) error { + f := cmd.Flags().Lookup(name) + if f == nil { + return fmt.Errorf("cannot override unknown flag --%s", name) + } + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + if required { + f.Annotations[AnnotationRequired] = []string{"true"} + } else { + delete(f.Annotations, AnnotationRequired) + } + if promptOptional { + f.Annotations[AnnotationPrompt] = []string{"true"} + } else { + delete(f.Annotations, AnnotationPrompt) + } + return nil +} + +func SetPromptOptional(cmd *cobra.Command, name string, promptOptional bool) error { + f := cmd.Flags().Lookup(name) + if f == nil { + return fmt.Errorf("cannot update unknown flag --%s", name) + } + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + if promptOptional { + f.Annotations[AnnotationPrompt] = []string{"true"} + } else { + delete(f.Annotations, AnnotationPrompt) + } + return nil +} + +func SetMetaPromptOptional(cmd *cobra.Command, meta []FlagMeta, promptOptional bool) { + for _, m := range meta { + if cmd.Flags().Lookup(m.FlagName) != nil { + _ = SetPromptOptional(cmd, m.FlagName, promptOptional) + } + if m.Union == nil { + continue + } + for _, variant := range m.Union.Variants { + if cmd.Flags().Lookup(variant.FlagName) != nil { + _ = SetPromptOptional(cmd, variant.FlagName, promptOptional) + } + SetMetaPromptOptional(cmd, variant.Fields, promptOptional) + } + } +} + +func ClearBodyRequirements(cmd *cobra.Command, meta []FlagMeta, bodyFieldPath string) { + for _, m := range meta { + if !isBodyFieldPath(m.FieldPath, bodyFieldPath) { + continue + } + clearMetaRequirements(cmd, m) + } +} + +func clearMetaRequirements(cmd *cobra.Command, m FlagMeta) { + if cmd.Flags().Lookup(m.FlagName) != nil { + _ = OverridePromptRequirement(cmd, m.FlagName, false, false) + } + if m.Union == nil { + return + } + for _, variant := range m.Union.Variants { + if cmd.Flags().Lookup(variant.FlagName) != nil { + _ = OverridePromptRequirement(cmd, variant.FlagName, false, false) + } + for _, field := range variant.Fields { + clearMetaRequirements(cmd, field) + } + } +} + +func MarkBodyFlag(cmd *cobra.Command, name string) error { + f := cmd.Flags().Lookup(name) + if f == nil { + return fmt.Errorf("cannot mark unknown body flag --%s", name) + } + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + f.Annotations[AnnotationBodyFlag] = []string{"true"} + if len(f.Annotations[AnnotationPromptKind]) == 0 { + f.Annotations[AnnotationPromptKind] = []string{"json"} + } + return nil +} + +func AnnotateBodyFields(cmd *cobra.Command, meta []FlagMeta, bodyFieldPath string, bodyFlags ...string) error { + for _, m := range meta { + if !isBodyFieldPath(m.FieldPath, bodyFieldPath) { + continue + } + annotateBodySource(cmd, m.FlagName, bodyFlags) + if m.Union == nil { + continue + } + for _, variant := range m.Union.Variants { + annotateBodySource(cmd, variant.FlagName, bodyFlags) + annotateAllBodySources(cmd, variant.Fields, bodyFlags) + } + } + return nil +} + +func annotateAllBodySources(cmd *cobra.Command, meta []FlagMeta, bodyFlags []string) { + for _, m := range meta { + annotateBodySource(cmd, m.FlagName, bodyFlags) + if m.Union == nil { + continue + } + for _, variant := range m.Union.Variants { + annotateBodySource(cmd, variant.FlagName, bodyFlags) + annotateAllBodySources(cmd, variant.Fields, bodyFlags) + } + } +} + +func annotateBodySource(cmd *cobra.Command, name string, bodyFlags []string) { + f := cmd.Flags().Lookup(name) + if f == nil { + return + } + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + f.Annotations[AnnotationBodyField] = append([]string(nil), bodyFlags...) +} + +func promptKindName(kind FlagKind) string { + switch kind { + case FlagKindBool: + return "bool" + case FlagKindInt64: + return "int64" + case FlagKindFloat64: + return "float64" + case FlagKindStringArray, FlagKindFileArray: + return "string-array" + case FlagKindEnum, FlagKindIntEnum: + return "enum" + case FlagKindJSON, FlagKindUnion: + return "json" + case FlagKindFile: + return "file" + default: + return "string" + } +} + +func promptMetaRequired(m FlagMeta) bool { + return m.Required +} + +// RegisterFlags registers Cobra flags from metadata, including union flags. +// Flags that already exist as inherited persistent flags (e.g., global security +// credentials) are skipped to prevent local flags from shadowing them. +func RegisterFlags(cmd *cobra.Command, meta []FlagMeta) { + for order, m := range meta { + // Skip if a flag with this name already exists — either inherited from + // a parent command (persistent globals/security) or already registered + // locally (e.g., operation security flags like username/password). + if cmd.InheritedFlags().Lookup(m.FlagName) != nil || cmd.Flags().Lookup(m.FlagName) != nil { + continue + } + + switch m.Kind { + case FlagKindUnion: + if m.Union != nil { + registerUnionFlags(cmd, m) + } + case FlagKindFile, FlagKindFileArray, FlagKindBytes: + registerStringFlag(cmd, m) + case FlagKindString, FlagKindEnum, FlagKindIntEnum, FlagKindJSON, FlagKindDateTime, FlagKindDate: + registerStringFlag(cmd, m) + case FlagKindBool: + if m.Shorthand != "" { + cmd.Flags().BoolP(m.FlagName, m.Shorthand, m.DefaultBool, m.Description) + } else { + cmd.Flags().Bool(m.FlagName, m.DefaultBool, m.Description) + } + case FlagKindInt64: + if m.Shorthand != "" { + cmd.Flags().Int64P(m.FlagName, m.Shorthand, m.DefaultInt, m.Description) + } else { + cmd.Flags().Int64(m.FlagName, m.DefaultInt, m.Description) + } + case FlagKindFloat64: + if m.Shorthand != "" { + cmd.Flags().Float64P(m.FlagName, m.Shorthand, m.DefaultFloat, m.Description) + } else { + cmd.Flags().Float64(m.FlagName, m.DefaultFloat, m.Description) + } + case FlagKindStringArray: + if m.Shorthand != "" { + cmd.Flags().StringArrayP(m.FlagName, m.Shorthand, nil, m.Description) + } else { + cmd.Flags().StringArray(m.FlagName, nil, m.Description) + } + } + + if cmd.Flags().Lookup(m.FlagName) != nil { + _ = AnnotatePromptFlag(cmd, m.FlagName, PromptFlagSpec{ + Required: promptMetaRequired(m), + PromptOptional: true, + Kind: promptKindName(m.Kind), + Values: m.EnumValues, + Order: order, + EnvVar: m.EnvVar, + ConfigKey: m.ConfigKey, + DefaultResolves: m.HasDefault, + }) + } + if m.Group != "" { + _ = cmd.Flags().SetAnnotation(m.FlagName, "speakeasy:group", []string{m.Group}) + } + } +} + +// registerStringFlag registers a string flag with optional shorthand. +func registerStringFlag(cmd *cobra.Command, m FlagMeta) { + if m.Shorthand != "" { + cmd.Flags().StringP(m.FlagName, m.Shorthand, m.DefaultStr, m.Description) + } else { + cmd.Flags().String(m.FlagName, m.DefaultStr, m.Description) + } +} + +// ValidateMeta checks that all FieldPaths in meta resolve to valid fields on T. +// Call from RegisterFlags or init() to catch template generation bugs at startup +// rather than at CLI runtime. +func ValidateMeta[T any](meta []FlagMeta) error { + var zero T + v := reflect.ValueOf(&zero).Elem() + for _, m := range meta { + if m.Kind == FlagKindUnion { + // Validate that the union FieldPath resolves; variant fields are validated at build time + if err := validateFieldPath(v, m.FieldPath); err != nil { + return fmt.Errorf("invalid metadata for flag --%s: %w", m.FlagName, err) + } + continue + } + if err := validateFieldPath(v, m.FieldPath); err != nil { + return fmt.Errorf("invalid metadata for flag --%s: %w", m.FlagName, err) + } + } + return nil +} + +// validateFieldPath checks that the dot-delimited path resolves to a valid field on v. +func validateFieldPath(v reflect.Value, path string) error { + parts := strings.Split(path, ".") + current := v + for _, part := range parts { + for current.Kind() == reflect.Ptr { + current = reflect.New(current.Type().Elem()).Elem() + } + if current.Kind() != reflect.Struct { + return fmt.Errorf("expected struct at %q, got %s", part, current.Kind()) + } + field := current.FieldByName(part) + if !field.IsValid() { + return fmt.Errorf("field %q not found (target type: %s)", part, current.Type()) + } + current = field + } + return nil +} + +// BuildRequest reads all flags described by meta, validates them, and populates a new T. +// Returns a pointer to the populated struct. Union fields are handled automatically +// via their embedded UnionMeta. +// +// bodyFieldPath identifies which field on T contains the request body (e.g. "BaseUser"). +// When the --body flag or stdin is piped, the JSON is unmarshaled into that sub-field first, +// then individual flags override specific fields (merge behavior). Empty string means the +// entire struct is the body (IsRequestBody path) or there is no body field (params-only). +// +// bodyFlagName is the name of the whole-body JSON flag (e.g. "body"). When non-empty and the +// flag is set, its value takes priority over stdin. Individual flags always override both. +func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath string, bodyFlagName string) (*T, error) { + var req T + v := reflect.ValueOf(&req).Elem() + bodyPrePopulated := false + hasRequestBody := bodyFieldPath != "" || bodyFlagName != "" || !isJSONSerialized(v.Type()) + + decodeBody := func(data []byte, source string) error { + u := bodyUnionMeta(meta, bodyFieldPath) + if bodyFieldPath != "" { + bodyField, err := navigateToField(v, bodyFieldPath) + if err != nil { + return fmt.Errorf("failed to resolve body field %q: %w", bodyFieldPath, err) + } + withDefaults := applyNestedUnionDefaults(applyUnionDefaults(data, u), meta, bodyFieldPath, bodyField.Type()) + if err := unmarshalIntoField(bodyField, withDefaults); err != nil { + return fmt.Errorf("failed to parse %s as JSON: %w", source, err) + } + return enforceStrictOrWarn(cmd, verifyBodyKeys(source, data, bodyField, u)) + } + withDefaults := applyNestedUnionDefaults(data, meta, "", reflect.TypeOf(req)) + if err := json.Unmarshal(withDefaults, &req); err != nil { + return fmt.Errorf("failed to parse %s as JSON: %w", source, err) + } + v = reflect.ValueOf(&req).Elem() // refresh after unmarshal + return enforceStrictOrWarn(cmd, verifyBodyKeys(source, data, v, nil)) + } + + wholeBodyFlag := "" + for _, m := range meta { + if bodyFieldPath != "" && m.FieldPath == bodyFieldPath && FlagChanged(cmd, m.FlagName) { + wholeBodyFlag = m.FlagName + } + } + + // Priority 1: --body flag + if bodyFlagName != "" && FlagChanged(cmd, bodyFlagName) { + if wholeBodyFlag != "" { + return nil, fmt.Errorf("--%s and --%s both supply the whole request body; pass exactly one", bodyFlagName, wholeBodyFlag) + } + bodyJSON, _ := GetStringFlag(cmd, bodyFlagName) + bodyJSON, err := ResolveBodyFlagValue(cmd, bodyFlagName, bodyJSON) + if err != nil { + return nil, err + } + if bodyJSON != "" { + if err := decodeBody([]byte(bodyJSON), "--"+bodyFlagName); err != nil { + return nil, err + } + bodyPrePopulated = true + } + } + + // Priority 2: stdin + if !bodyPrePopulated && bodyFlagName != "" && wholeBodyFlag == "" { + stdinData, err := ReadStdinBody(cmd, bodyFlagName) + if err != nil { + return nil, err + } + if len(bytes.TrimSpace(stdinData)) > 0 { + if err := decodeBody(stdinData, "stdin"); err != nil { + return nil, err + } + bodyPrePopulated = true + } + } + + // When body provided via --body flag or stdin, relax Required checks for body fields + // so builders don't error for fields already populated + if bodyPrePopulated { + meta = relaxRequiredForBodyFields(meta, bodyFieldPath, true) + } + + // When the entire struct IS the body (bodyFieldPath == "") and no body was + // provided via --body/stdin, check if any individual flags were changed. + // If not, the user didn't attempt to provide a body at all — relax required + // checks so nullable/optional bodies work without erroring on inner required fields. + if !bodyPrePopulated && bodyFieldPath == "" { + anyChanged := false + for _, m := range meta { + if FlagChanged(cmd, m.FlagName) { + anyChanged = true + break + } + } + if !anyChanged { + meta = relaxRequiredForBodyFields(meta, "", false) + } + } + + // Priority 3: individual flags — only Changed() flags modify the struct + for _, m := range meta { + var err error + if m.Kind == FlagKindUnion { + if m.Union != nil { + err = buildUnionField(cmd, v, m) + } + } else { + err = buildFieldOnValue(cmd, v, m) + } + if err != nil { + return nil, err + } + } + + if hasRequestBody { + body := v + if bodyFieldPath != "" { + var err error + body, err = navigateToField(v, bodyFieldPath) + if err != nil { + return nil, fmt.Errorf("failed to resolve body field %q: %w", bodyFieldPath, err) + } + } + if err := validateRequiredUnionFields(body, meta, bodyFieldPath, bodyFlagName); err != nil { + return nil, err + } + } + + return &req, nil +} + +// BuildRequestBody reads a request body from a flag or stdin, unmarshals JSON, and returns *T. +// This handles the IsRequestBody pattern where the entire body is a single JSON blob. +// When isRequired is false and no input is provided, returns (nil, nil) instead of an error. +func BuildRequestBody[T any](cmd *cobra.Command, flagName string, annotations string, isRequired bool) (*T, error) { + var requestData string + + if FlagChanged(cmd, flagName) { + requestData, _ = GetStringFlag(cmd, flagName) + resolved, err := ResolveBodyFlagValue(cmd, flagName, requestData) + if err != nil { + return nil, err + } + requestData = resolved + } else { + stdin, err := ReadStdinBody(cmd, flagName) + if err != nil { + return nil, err + } + requestData = strings.TrimSpace(string(stdin)) + } + + if requestData == "" { + if isRequired { + return nil, &MissingRequiredFlagError{FlagName: flagName, Detail: "(or provide via stdin)"} + } + return nil, nil + } + + var req T + // Special case: when T is []byte, treat input as raw bytes rather than JSON. + // Go's json.Unmarshal treats []byte as a JSON array of numbers, not a string. + if reflect.TypeOf(&req).Elem().Kind() == reflect.Slice && reflect.TypeOf(&req).Elem().Elem().Kind() == reflect.Uint8 { + raw := reflect.ValueOf([]byte(requestData)) + reflect.ValueOf(&req).Elem().Set(raw) + return &req, nil + } + if err := utils.UnmarshalJsonFromString(requestData, &req, annotations); err != nil { + return nil, fmt.Errorf("invalid %s: %w", flagName, err) + } + source := "stdin" + if FlagChanged(cmd, flagName) { + source = "--" + flagName + } + if err := enforceStrictOrWarn(cmd, verifyBodyKeys(source, []byte(requestData), reflect.ValueOf(&req).Elem(), nil)); err != nil { + return nil, err + } + if err := validateRequiredUnionFields(reflect.ValueOf(&req).Elem(), nil, "", flagName); err != nil { + return nil, err + } + return &req, nil +} + +type MissingRequiredFieldError struct { + Path string + FlagHint string + BodyFlagName string +} + +func (e *MissingRequiredFieldError) Error() string { + if e.Path == "" { + unionOptions := strings.Replace(e.FlagHint, ", or ", " / ", 1) + switch { + case e.BodyFlagName != "" && unionOptions != "": + return fmt.Sprintf("the request body is missing or selects no variant; pass --%s (or %s)", e.BodyFlagName, unionOptions) + case e.BodyFlagName != "": + return fmt.Sprintf("the request body is missing or selects no variant; pass --%s", e.BodyFlagName) + case unionOptions != "": + return fmt.Sprintf("the request body is missing or selects no variant; pass %s", unionOptions) + default: + return "the request body is missing or selects no variant; provide a body that selects one variant" + } + } + if e.FlagHint != "" { + return fmt.Sprintf("missing required field %q (%s)", e.Path, e.FlagHint) + } + return fmt.Sprintf("missing required field %q", e.Path) +} + +func (*MissingRequiredFieldError) CLIReason() string { return "CLI_VALIDATION" } + +// A union serializes as its selected member's wire shape, so members share the parent JSON path. +func validateRequiredUnionFields(body reflect.Value, meta []FlagMeta, bodyFieldPath, bodyFlagName string) error { + err := walkRequiredUnionFields(body, meta, bodyFieldPath, "") + var missing *MissingRequiredFieldError + if errors.As(err, &missing) && missing.Path == "" { + missing.BodyFlagName = bodyFlagName + } + return err +} + +func walkRequiredUnionFields(v reflect.Value, meta []FlagMeta, goPath, jsonPath string) error { + for v.IsValid() && (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) { + if v.IsNil() { + return nil + } + v = v.Elem() + } + if !v.IsValid() { + return nil + } + + switch v.Kind() { + case reflect.Struct: + if union, selected := selectedUnionMembers(v); union { + if len(selected) == 0 && !hasUnknownUnionValue(v) { + return &MissingRequiredFieldError{Path: jsonPath, FlagHint: unionFlagHint(meta, goPath)} + } + for _, member := range selected { + if err := walkRequiredUnionFields(member, meta, goPath, jsonPath); err != nil { + return err + } + } + return nil + } + for i := 0; i < v.NumField(); i++ { + fieldType := v.Type().Field(i) + if fieldType.PkgPath != "" || jsonFieldOmitted(fieldType, v.Field(i)) { + continue + } + jsonName := serializedBodyFieldName(fieldType) + if jsonName == "" { + continue + } + if err := walkRequiredUnionFields( + v.Field(i), + meta, + joinFieldPath(goPath, fieldType.Name), + joinJSONPath(jsonPath, jsonName), + ); err != nil { + return err + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + if err := walkRequiredUnionFields(v.Index(i), meta, goPath, fmt.Sprintf("%s[%d]", jsonPath, i)); err != nil { + return err + } + } + case reflect.Map: + if v.IsNil() { + return nil + } + iter := v.MapRange() + for iter.Next() { + path := fmt.Sprintf("%s[%v]", jsonPath, iter.Key().Interface()) + if err := walkRequiredUnionFields(iter.Value(), meta, goPath, path); err != nil { + return err + } + } + } + return nil +} + +// jsonFieldOmitted mirrors the generated SDK's utils.MarshalJSON omission rules. +func jsonFieldOmitted(field reflect.StructField, value reflect.Value) bool { + if field.Tag.Get("const") != "" { + return false + } + var omitEmpty, omitZero bool + for _, option := range strings.Split(field.Tag.Get("json"), ",")[1:] { + switch option { + case "omitempty": + omitEmpty = true + case "omitzero": + omitZero = true + } + } + if omitZero && value.IsZero() { + return true + } + if !omitEmpty { + return false + } + switch value.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + if value.IsNil() { + return true + } + } + if value.Kind() != reflect.Struct && value.IsZero() { + return true + } + switch value.Kind() { + case reflect.Array, reflect.Map, reflect.Slice: + return value.Len() == 0 + } + return false +} + +func serializedBodyFieldName(field reflect.StructField) string { + if tag := field.Tag.Get("json"); tag != "" { + name := strings.Split(tag, ",")[0] + if name == "-" { + return "" + } + return name + } + for _, key := range []string{"form", "multipartForm"} { + tag := field.Tag.Get(key) + if tag == "" { + continue + } + var name string + jsonEncoded := false + for _, part := range strings.Split(tag, ",") { + if part == "json" { + jsonEncoded = true + } else if strings.HasPrefix(part, "name=") { + name = strings.TrimPrefix(part, "name=") + } + } + if jsonEncoded { + return name + } + } + return "" +} + +func selectedUnionMembers(v reflect.Value) (bool, []reflect.Value) { + isUnion := false + var selected []reflect.Value + for i := 0; i < v.NumField(); i++ { + if v.Type().Field(i).Tag.Get("union") != "member" { + continue + } + isUnion = true + member := v.Field(i) + if unionMemberIsSet(member) { + selected = append(selected, member) + } + } + return isUnion, selected +} + +func unionMemberIsSet(v reflect.Value) bool { + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return !v.IsNil() + default: + return !v.IsZero() + } +} + +func hasUnknownUnionValue(v reflect.Value) bool { + for i := 0; i < v.NumField(); i++ { + if v.Type().Field(i).Tag.Get("union") != "unknown" { + continue + } + raw := v.Field(i) + return (raw.Kind() == reflect.Slice || raw.Kind() == reflect.Array || raw.Kind() == reflect.String) && raw.Len() > 0 + } + return false +} + +func unionFlagHint(meta []FlagMeta, goPath string) string { + for _, m := range meta { + if m.Kind != FlagKindUnion || m.FieldPath != goPath { + continue + } + parts := []string{"--" + m.FlagName} + if m.Union != nil { + var variants []string + for _, variant := range m.Union.Variants { + name := "--" + variant.FlagName + if variant.CanExpand && len(variant.Fields) > 0 { + name += ".*" + } + variants = append(variants, name) + } + if len(variants) > 0 { + parts = append(parts, "or "+strings.Join(variants, " / ")) + } + } + return strings.Join(parts, ", ") + } + return "" +} + +func joinFieldPath(base, field string) string { + if base == "" { + return field + } + return base + "." + field +} + +func joinJSONPath(base, field string) string { + if base == "" { + return field + } + return base + "." + field +} + +func strictBodyKeys(cmd *cobra.Command) bool { + if cmd == nil { + return false + } + if cmd.Annotations["speakeasy_strict_body_keys"] == "true" { + return true + } + val, changed := GetBoolFlag(cmd, "agent-mode") + return changed && val +} + +type unionBodyKeyError struct{ err error } + +func (e *unionBodyKeyError) Error() string { return e.err.Error() } +func (e *unionBodyKeyError) Unwrap() error { return e.err } + +func enforceStrictOrWarn(cmd *cobra.Command, err error) error { + if err == nil || strictBodyKeys(cmd) { + return err + } + var unionErr *unionBodyKeyError + if errors.As(err, &unionErr) { + return err + } + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %v\n", err) + return nil +} + +func bodyTypeIsUnion(t reflect.Type) bool { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return false + } + for i := 0; i < t.NumField(); i++ { + if _, ok := t.Field(i).Tag.Lookup("union"); ok { + return true + } + } + return false +} + +func verifyBodyKeys(source string, input []byte, target reflect.Value, u *UnionMeta) error { + var in map[string]json.RawMessage + if err := json.Unmarshal(input, &in); err != nil || len(in) == 0 { + return nil // not an object (or empty): nothing to lose + } + if !isJSONSerialized(target.Type()) { + return nil // form-only body types have no JSON wire shape + } + if target.CanAddr() { + target = target.Addr() + } + encoded, err := utils.MarshalJSON(target.Interface(), "", true) + if err != nil { + return nil + } + var out map[string]json.RawMessage + if err := json.Unmarshal(encoded, &out); err != nil { + return nil + } + // The serializer omits null optionals, so a known key set to null never survives into out. + knownForNull := knownJSONKeys(target.Type()) + var lost []string + for k, v := range in { + if _, kept := out[k]; kept { + continue + } + if string(bytes.TrimSpace(v)) == "null" && slices.Contains(knownForNull, k) { + continue + } + lost = append(lost, k) + } + if len(lost) == 0 { + return nil + } + slices.Sort(lost) + + if u != nil { + for _, k := range lost { + if !slices.Contains(u.VariantKeys, k) { + continue + } + for _, other := range u.VariantKeys { + if _, present := in[other]; present && other != k { + return &unionBodyKeyError{err: fmt.Errorf("%s names both %q and %q, but the request variant it selects carries only one of them; keep exactly one", source, other, k)} + } + } + } + } + + known := knownForNull + quoted := make([]string, 0, len(lost)) + var hints []string + for _, k := range lost { + quoted = append(quoted, strconv.Quote(k)) + if slices.Contains(known, k) { + hints = append(hints, fmt.Sprintf("%q belongs to a different request variant than the rest of the body", k)) + } else if s := closestKey(k, known); s != "" { + hints = append(hints, fmt.Sprintf("did you mean %q instead of %q?", s, k)) + } + } + noun := "key %s is" + if len(lost) > 1 { + noun = "keys %s are" + } + msg := fmt.Sprintf("%s: "+noun+" not part of the request body and would be dropped", source, strings.Join(quoted, ", ")) + if len(hints) > 0 { + msg += " (" + strings.Join(hints, "; ") + ")" + } + if u != nil || bodyTypeIsUnion(target.Type()) { + return &unionBodyKeyError{err: fmt.Errorf("%s", msg)} + } + return fmt.Errorf("%s", msg) +} + +func isJSONSerialized(t reflect.Type) bool { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return true + } + formTagged := false + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.Tag.Get("union") == "member" && isJSONSerialized(f.Type) { + return true + } + if name := strings.Split(f.Tag.Get("json"), ",")[0]; name != "" && name != "-" { + return true + } + if f.Tag.Get("form") != "" || f.Tag.Get("multipartForm") != "" { + formTagged = true + } + } + return !formTagged +} + +func knownJSONKeys(t reflect.Type) []string { + seen := map[string]bool{} + var keys []string + var walk func(t reflect.Type, depth int) + walk = func(t reflect.Type, depth int) { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct || depth > 4 { + return + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.Tag.Get("union") == "member" { + walk(f.Type, depth+1) + continue + } + name := strings.Split(f.Tag.Get("json"), ",")[0] + if name == "" || name == "-" || seen[name] { + continue + } + seen[name] = true + keys = append(keys, name) + } + } + walk(t, 0) + slices.Sort(keys) + return keys +} + +func closestKey(lost string, known []string) string { + fold := func(s string) string { + return strings.NewReplacer("_", "", "-", "").Replace(strings.ToLower(s)) + } + target := fold(lost) + limit := 1 + if len([]rune(target)) >= 4 { + limit = 2 + } + best, bestDist := "", limit+1 + for _, k := range known { + if d := editDistance(target, fold(k)); d < bestDist { + best, bestDist = k, d + } + } + return best +} + +func editDistance(a, b string) int { + ra, rb := []rune(a), []rune(b) + prev := make([]int, len(rb)+1) + cur := make([]int, len(rb)+1) + for j := range prev { + prev[j] = j + } + for i := 1; i <= len(ra); i++ { + cur[0] = i + for j := 1; j <= len(rb); j++ { + cost := 1 + if ra[i-1] == rb[j-1] { + cost = 0 + } + cur[j] = min(prev[j]+1, cur[j-1]+1, prev[j-1]+cost) + } + prev, cur = cur, prev + } + return prev[len(rb)] +} + +// unmarshalIntoField unmarshals JSON data into a reflect.Value field, +// handling both pointer and value types. +func unmarshalIntoField(field reflect.Value, data []byte) error { + fieldType := field.Type() + isPtr := fieldType.Kind() == reflect.Ptr + if isPtr { + fieldType = fieldType.Elem() + } + target := reflect.New(fieldType) + if err := json.Unmarshal(data, target.Interface()); err != nil { + return err + } + if isPtr { + field.Set(target) + } else { + field.Set(target.Elem()) + } + return nil +} + +func relaxRequiredForBodyFields(meta []FlagMeta, bodyFieldPath string, clearDefaults bool) []FlagMeta { + result := make([]FlagMeta, len(meta)) + copy(result, meta) + for i := range result { + if isBodyFieldPath(result[i].FieldPath, bodyFieldPath) { + result[i].Required = false + if clearDefaults { + result[i].HasDefault = false + } + // Also relax union fields so buildUnionField doesn't error + if result[i].Union != nil { + unionCopy := *result[i].Union + unionCopy.Optional = true + result[i].Union = &unionCopy + } + } + } + return result +} + +func NonBodyMeta(meta []FlagMeta, bodyFieldPath string) []FlagMeta { + var result []FlagMeta + for _, m := range meta { + if !isBodyFieldPath(m.FieldPath, bodyFieldPath) { + result = append(result, m) + } + } + return result +} + +// isBodyFieldPath returns true if the given field path belongs to the body sub-struct. +func isBodyFieldPath(fieldPath, bodyFieldPath string) bool { + if bodyFieldPath == "" { + return true // entire struct is body + } + return fieldPath == bodyFieldPath || strings.HasPrefix(fieldPath, bodyFieldPath+".") +} + +// setFieldByPath navigates nested struct fields via a dot-delimited path and sets the leaf value. +// It auto-allocates nil pointer intermediates and wraps leaf values in pointers for optional fields. +func setFieldByPath(v reflect.Value, path string, val reflect.Value) error { + parts := strings.Split(path, ".") + current := v + for i, part := range parts { + // Dereference pointers, allocating nil ones + for current.Kind() == reflect.Ptr { + if current.IsNil() { + if !current.CanSet() { + return fmt.Errorf("cannot set nil pointer at %q in path %q", part, path) + } + current.Set(reflect.New(current.Type().Elem())) + } + current = current.Elem() + } + if current.Kind() != reflect.Struct { + return fmt.Errorf("expected struct at %q in path %q, got %s", part, path, current.Kind()) + } + field := current.FieldByName(part) + if !field.IsValid() { + return fmt.Errorf("field %q not found at path %q (target type: %s)", part, path, current.Type()) + } + if !field.CanSet() { + return fmt.Errorf("field %q is unexported or unaddressable at path %q", part, path) + } + if i == len(parts)-1 { + // Leaf — set value + if field.Kind() == reflect.Ptr { + // Optional field — allocate pointer and set pointee + elemType := field.Type().Elem() + if !val.Type().ConvertibleTo(elemType) { + return fmt.Errorf("cannot convert %s to %s for field %q at path %q", + val.Type(), elemType, part, path) + } + ptr := reflect.New(elemType) + ptr.Elem().Set(val.Convert(elemType)) + field.Set(ptr) + } else if field.Kind() == reflect.Slice && val.Kind() == reflect.Slice { + // Slice field — convert elements if needed + if val.Type().AssignableTo(field.Type()) { + field.Set(val) + } else { + // Element-wise conversion (e.g., []string → []EnumType) + elemType := field.Type().Elem() + newSlice := reflect.MakeSlice(field.Type(), val.Len(), val.Len()) + for j := 0; j < val.Len(); j++ { + if !val.Index(j).Type().ConvertibleTo(elemType) { + return fmt.Errorf("cannot convert slice element %s to %s for field %q at path %q", + val.Index(j).Type(), elemType, part, path) + } + newSlice.Index(j).Set(val.Index(j).Convert(elemType)) + } + field.Set(newSlice) + } + } else { + if !val.Type().ConvertibleTo(field.Type()) { + return fmt.Errorf("cannot convert %s to %s for field %q at path %q", + val.Type(), field.Type(), part, path) + } + field.Set(val.Convert(field.Type())) + } + } else { + current = field + } + } + return nil +} + +// navigateToField resolves a dot-delimited field path and returns the leaf field. +// This is used by JSON handling to get the field type for unmarshal. +func navigateToField(v reflect.Value, path string) (reflect.Value, error) { + parts := strings.Split(path, ".") + current := v + for _, part := range parts { + for current.Kind() == reflect.Ptr { + if current.IsNil() { + if !current.CanSet() { + return reflect.Value{}, fmt.Errorf("cannot set nil pointer at %q in path %q", part, path) + } + current.Set(reflect.New(current.Type().Elem())) + } + current = current.Elem() + } + if current.Kind() != reflect.Struct { + return reflect.Value{}, fmt.Errorf("expected struct at %q in path %q, got %s", part, path, current.Kind()) + } + field := current.FieldByName(part) + if !field.IsValid() { + return reflect.Value{}, fmt.Errorf("field %q not found at path %q (target type: %s)", part, path, current.Type()) + } + current = field + } + return current, nil +} + +// Per-kind builder helpers. + +// shouldSkipUnchanged returns true when an optional field with no default +// was not explicitly set by the user (leave nil). +func shouldSkipUnchanged(m FlagMeta, changed bool) bool { + return m.Optional && !changed && !m.HasDefault +} + +// shouldSkipDefault returns true when a non-required field with no default +// was not explicitly set (leave zero value). +func shouldSkipDefault(m FlagMeta, changed bool) bool { + return !changed && !m.Required && !m.HasDefault +} + +// validateRequiredString returns an error if a required string flag is empty. +func validateRequiredString(m FlagMeta, val string) error { + if m.Required && val == "" { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + return nil +} + +func validateStringLength(m FlagMeta, val string, changed bool) error { + if !changed || m.MinLength <= 0 { + return nil + } + if val == "" { + return fmt.Errorf("invalid value for --%s: empty (the schema requires at least %d character(s)); omit the flag to leave the field unset", m.FlagName, m.MinLength) + } + if int64(utf8.RuneCountInString(val)) < m.MinLength { + return fmt.Errorf("invalid value for --%s: %q is shorter than the minimum length %d", m.FlagName, val, m.MinLength) + } + return nil +} + +// pflag's float64 parser accepts NaN and Inf, which are not valid JSON numbers. +func validateFiniteNumber(m FlagMeta, val float64, changed bool) error { + if changed && (math.IsNaN(val) || math.IsInf(val, 0)) { + return fmt.Errorf("invalid value for --%s: %s is not a finite number", m.FlagName, formatNumber(val)) + } + return nil +} + +func validateNumericBounds(m FlagMeta, val float64, changed bool) error { + if !changed { + return nil + } + if m.HasMinimum && val < m.Minimum { + return fmt.Errorf("invalid value for --%s: %s is below the minimum %s", m.FlagName, formatNumber(val), formatNumber(m.Minimum)) + } + if m.HasMaximum && val > m.Maximum { + return fmt.Errorf("invalid value for --%s: %s is above the maximum %s", m.FlagName, formatNumber(val), formatNumber(m.Maximum)) + } + return nil +} + +func formatNumber(f float64) string { + return strconv.FormatFloat(f, 'f', -1, 64) +} + +func validateRequiredPresence(m FlagMeta, changed bool) error { + if m.Required && !changed { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + return nil +} + +func validateEnumValue(m FlagMeta, val string, changed bool) error { + if m.EnumValues == nil { + return nil + } + if val == "" && !changed { + return nil + } + if slices.Contains(m.EnumValues, val) { + return nil + } + if val == "" { + return fmt.Errorf("invalid value for --%s: empty; valid options: %s (omit the flag to leave the field unset)", + m.FlagName, strings.Join(m.EnumValues, ", ")) + } + return fmt.Errorf("invalid value for --%s: %q; valid options: %s", + m.FlagName, val, strings.Join(m.EnumValues, ", ")) +} + +// Per-kind builder functions. Each reads the flag value, validates, and sets via reflection. + +func buildStringField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + if err := enforceStrictOrWarn(cmd, validateStringLength(m, val, changed)); err != nil { + return err + } + if err := validateRequiredPresence(m, changed); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildBoolField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetBoolFlag(cmd, m.FlagName) + if m.Required && !changed { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildInt64Field(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetInt64Flag(cmd, m.FlagName) + if m.Required && !changed { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + if err := validateFiniteNumber(m, float64(val), changed); err != nil { + return err + } + if err := enforceStrictOrWarn(cmd, validateNumericBounds(m, float64(val), changed)); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildFloat64Field(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetFloat64Flag(cmd, m.FlagName) + if m.Required && !changed { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + if err := validateFiniteNumber(m, val, changed); err != nil { + return err + } + if err := enforceStrictOrWarn(cmd, validateNumericBounds(m, val, changed)); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildStringArrayField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringArrayFlag(cmd, m.FlagName) + + if m.Required && len(val) == 0 { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + + if !changed { + return nil + } + + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildDateTimeField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + if err := validateRequiredString(m, val); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + if val == "" { + return nil + } + + // Validate and parse RFC3339 format + t, err := time.Parse(time.RFC3339Nano, val) + if err != nil { + t, err = time.Parse(time.RFC3339, val) + if err != nil { + return fmt.Errorf("invalid value for --%s: expected RFC3339 format (e.g., 2024-01-15T10:30:00Z), got %q", m.FlagName, val) + } + } + + // If the field type is time.Time, set the parsed value directly + field, navErr := navigateToField(v, m.FieldPath) + if navErr == nil { + timeType := reflect.TypeOf(time.Time{}) + if field.Type() == timeType { + field.Set(reflect.ValueOf(t)) + return nil + } + if field.Kind() == reflect.Ptr && field.Type().Elem() == timeType { + field.Set(reflect.ValueOf(&t)) + return nil + } + } + + // For string-typed fields, store as-is + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildDateField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + if err := validateRequiredString(m, val); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + if val == "" { + return nil + } + + // Validate date format (YYYY-MM-DD) + if _, err := time.Parse("2006-01-02", val); err != nil { + return fmt.Errorf("invalid value for --%s: expected date format (e.g., 2024-01-15), got %q", m.FlagName, val) + } + + // If the field type is types.Date, parse and set directly + field, navErr := navigateToField(v, m.FieldPath) + if navErr == nil { + dateType := reflect.TypeOf(types.Date{}) + if field.Type() == dateType { + d, err := types.DateFromString(val) + if err != nil { + return fmt.Errorf("invalid value for --%s: %w", m.FlagName, err) + } + field.Set(reflect.ValueOf(d)) + return nil + } + if field.Kind() == reflect.Ptr && field.Type().Elem() == dateType { + d, err := types.DateFromString(val) + if err != nil { + return fmt.Errorf("invalid value for --%s: %w", m.FlagName, err) + } + field.Set(reflect.ValueOf(&d)) + return nil + } + } + + // For string-typed fields, store as-is + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildEnumField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + if err := validateEnumValue(m, val, changed); err != nil { + return err + } + if err := validateRequiredPresence(m, changed); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + + // reflect.Convert handles string → EnumType since they share underlying type + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(val)) +} + +func buildIntEnumField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + if err := validateEnumValue(m, val, changed); err != nil { + return err + } + if err := validateRequiredString(m, val); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + + // Parse string to int64 + if val == "" { + return nil + } + intVal, err := strconv.ParseInt(val, 10, 64) + if err != nil { + validStr := strings.Join(m.EnumValues, ", ") + return fmt.Errorf("expected integer enum value for --%s: valid values: %s", m.FlagName, validStr) + } + + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(intVal)) +} + +func buildJSONField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + + if m.Required && val == "" { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + + if m.Optional && !changed { + return nil + } + + if val == "" { + return nil + } + + // Navigate to the target field to get its type + field, err := navigateToField(v, m.FieldPath) + if err != nil { + return fmt.Errorf("failed to navigate to field for --%s: %w", m.FlagName, err) + } + + // Determine the concrete type (dereference pointer if needed) + fieldType := field.Type() + isPtr := fieldType.Kind() == reflect.Ptr + if isPtr { + fieldType = fieldType.Elem() + } + + // JSON null: handle nullable fields where the CLI passes --field null. + // For types implementing json.Unmarshaler (e.g., OptionalNullable), call the + // custom unmarshaler directly — Go's json.Unmarshal sets pointers to nil for + // "null" input, bypassing the custom UnmarshalJSON which distinguishes + // "explicitly null" from "unset". + if val == "null" { + target := reflect.New(field.Type()) + if unmarshaler, ok := target.Interface().(json.Unmarshaler); ok { + if err := unmarshaler.UnmarshalJSON([]byte("null")); err != nil { + return fmt.Errorf("invalid value for --%s: %w", m.FlagName, err) + } + field.Set(target.Elem()) + return nil + } + // For non-Unmarshaler types: set pointer/interface fields to zero (nil) + if isPtr || fieldType.Kind() == reflect.Interface { + field.Set(reflect.Zero(field.Type())) + return nil + } + } + + // For interface{} fields, use standard json.Unmarshal since the SDK's custom + // unmarshaler doesn't handle unaddressable interface values. + if fieldType.Kind() == reflect.Interface { + var parsed interface{} + if err := json.Unmarshal([]byte(val), &parsed); err != nil { + // Not valid JSON — treat as raw string + parsed = val + } + field.Set(reflect.ValueOf(parsed)) + return nil + } + + // If the annotation specifies bigint:"string" or decimal:"string", the SDK's + // unmarshalValue expects the value as a JSON string (e.g., "123"), not a bare + // number. Wrap bare numbers in JSON quotes for user convenience. + tag := reflect.StructTag(m.Annotations) + if (tag.Get("bigint") == "string" || tag.Get("decimal") == "string") && val != "" && val[0] != '"' && val[0] != '[' && val[0] != '{' { + val = `"` + val + `"` + } + + // Unmarshal the JSON value into the field. + // For pointer fields (e.g., *big.Int, *ModelType), we create a double-pointer + // (**T) with a pre-initialized inner pointer (*T = &T{}). This ensures: + // 1. The SDK's unmarshalValue can find a settable *T via v.Elem() + // 2. dereferencePointers can walk through both levels without nil panics + // 3. Model type fields are addressable for field-by-field unmarshaling + if isPtr { + holder := reflect.New(reflect.PtrTo(fieldType)) + holder.Elem().Set(reflect.New(fieldType)) + if err := utils.UnmarshalJsonFromString(val, holder.Interface(), m.Annotations); err != nil { + return fmt.Errorf("invalid value for --%s: %w", m.FlagName, err) + } + field.Set(holder.Elem()) + } else { + target := reflect.New(fieldType) + if err := utils.UnmarshalJsonFromString(val, target.Interface(), m.Annotations); err != nil { + return fmt.Errorf("invalid value for --%s: %w", m.FlagName, err) + } + field.Set(target.Elem()) + } + + return nil +} + +func buildFileField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + path, changed := GetStringFlag(cmd, m.FlagName) + + if m.Required && (!changed || path == "") { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + + if !changed || path == "" { + return nil // Optional and not provided + } + + // Read file from disk + content, err := os.ReadFile(path) + if err != nil { + return WithCLIValidation(fmt.Errorf("failed to read file for --%s: %w", m.FlagName, err)) + } + fileName := filepath.Base(path) + + // Navigate to the file struct field + field, navErr := navigateToField(v, m.FieldPath) + if navErr != nil { + return fmt.Errorf("failed to navigate to field for --%s: %w", m.FlagName, navErr) + } + + // Determine concrete struct type (handle optional *FileType vs FileType) + fieldType := field.Type() + isPtr := fieldType.Kind() == reflect.Ptr + if isPtr { + fieldType = fieldType.Elem() + } + + // Create file struct and set Content + FileName via reflection + fileStruct := reflect.New(fieldType).Elem() + if cf := fileStruct.FieldByName("Content"); cf.IsValid() && cf.CanSet() { + cf.SetBytes(content) + } + if nf := fileStruct.FieldByName("FileName"); nf.IsValid() && nf.CanSet() { + nf.SetString(fileName) + } + + if isPtr { + ptr := reflect.New(fieldType) + ptr.Elem().Set(fileStruct) + field.Set(ptr) + } else { + field.Set(fileStruct) + } + + return nil +} + +func buildFileArrayField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + + if m.Required && (!changed || val == "") { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + + if !changed || val == "" { + return nil + } + + // Split comma-separated file paths, supporting optional ;fileName= overrides + paths := strings.Split(val, ",") + + // Navigate to the slice field + field, navErr := navigateToField(v, m.FieldPath) + if navErr != nil { + return fmt.Errorf("failed to navigate to field for --%s: %w", m.FlagName, navErr) + } + + // Get the element type of the slice + sliceType := field.Type() + if sliceType.Kind() != reflect.Slice { + return fmt.Errorf("field %s is not a slice type for --%s", m.FieldPath, m.FlagName) + } + elemType := sliceType.Elem() + + result := reflect.MakeSlice(sliceType, 0, len(paths)) + + for _, entry := range paths { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + + // Parse optional ;fileName=override syntax + filePath := entry + fileNameOverride := "" + if idx := strings.Index(entry, ";fileName="); idx >= 0 { + filePath = entry[:idx] + fileNameOverride = entry[idx+len(";fileName="):] + } + + content, err := os.ReadFile(filePath) + if err != nil { + return WithCLIValidation(fmt.Errorf("failed to read file %q for --%s: %w", filePath, m.FlagName, err)) + } + + fileName := filepath.Base(filePath) + if fileNameOverride != "" { + fileName = fileNameOverride + } + + fileStruct := reflect.New(elemType).Elem() + if cf := fileStruct.FieldByName("Content"); cf.IsValid() && cf.CanSet() { + cf.SetBytes(content) + } + if nf := fileStruct.FieldByName("FileName"); nf.IsValid() && nf.CanSet() { + nf.SetString(fileName) + } + + result = reflect.Append(result, fileStruct) + } + + field.Set(result) + return nil +} + +func buildBytesField(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + val, changed := GetStringFlag(cmd, m.FlagName) + if err := validateRequiredString(m, val); err != nil { + return err + } + if shouldSkipUnchanged(m, changed) { + return nil + } + if shouldSkipDefault(m, changed) { + return nil + } + if val == "" { + return nil + } + + var data []byte + switch { + case strings.HasPrefix(val, "file:"): + path := strings.TrimPrefix(val, "file:") + var err error + data, err = os.ReadFile(path) + if err != nil { + return WithCLIValidation(fmt.Errorf("failed to read file for --%s: %w", m.FlagName, err)) + } + case strings.HasPrefix(val, "b64:"): + raw := strings.TrimPrefix(val, "b64:") + var err error + data, err = base64.StdEncoding.DecodeString(raw) + if err != nil { + // Try raw (no padding), URL-safe, and raw URL-safe variants + data, err = base64.RawStdEncoding.DecodeString(raw) + if err != nil { + data, err = base64.URLEncoding.DecodeString(raw) + if err != nil { + data, err = base64.RawURLEncoding.DecodeString(raw) + if err != nil { + return fmt.Errorf("invalid base64 for --%s: %w", m.FlagName, err) + } + } + } + } + default: + data = []byte(val) + } + + return setFieldByPath(v, m.FieldPath, reflect.ValueOf(data)) +} + +// buildFieldOnValue dispatches to the appropriate per-kind builder. +// Used by BuildRequest for non-union fields and by buildUnionField for variant fields. +func buildFieldOnValue(cmd *cobra.Command, v reflect.Value, m FlagMeta) error { + switch m.Kind { + case FlagKindString: + return buildStringField(cmd, v, m) + case FlagKindBool: + return buildBoolField(cmd, v, m) + case FlagKindInt64: + return buildInt64Field(cmd, v, m) + case FlagKindFloat64: + return buildFloat64Field(cmd, v, m) + case FlagKindStringArray: + return buildStringArrayField(cmd, v, m) + case FlagKindDateTime: + return buildDateTimeField(cmd, v, m) + case FlagKindDate: + return buildDateField(cmd, v, m) + case FlagKindEnum: + return buildEnumField(cmd, v, m) + case FlagKindIntEnum: + return buildIntEnumField(cmd, v, m) + case FlagKindJSON: + return buildJSONField(cmd, v, m) + case FlagKindFile: + return buildFileField(cmd, v, m) + case FlagKindFileArray: + return buildFileArrayField(cmd, v, m) + case FlagKindBytes: + return buildBytesField(cmd, v, m) + } + return nil +} + +// registerUnionFlags registers all Cobra flags for a union field from its UnionMeta. +func registerUnionFlags(cmd *cobra.Command, m FlagMeta) { + u := m.Union + // Top-level JSON flag (skip if inherited persistent flag exists) + if cmd.InheritedFlags().Lookup(m.FlagName) == nil { + if m.Shorthand != "" { + cmd.Flags().StringP(m.FlagName, m.Shorthand, "", u.TypeDescription) + } else { + cmd.Flags().String(m.FlagName, "", u.TypeDescription) + } + } + + // Per-variant flags (discriminated only) + for _, v := range u.Variants { + // Variant-level JSON flag (skip if inherited persistent flag exists) + if cmd.InheritedFlags().Lookup(v.FlagName) == nil { + cmd.Flags().String(v.FlagName, "", v.Description) + } + annotateUnionMember(cmd, v.FlagName, m.FlagName) + + // Expanded variant fields reuse RegisterFlags + if v.CanExpand && len(v.Fields) > 0 { + RegisterFlags(cmd, v.Fields) + annotateUnionMembers(cmd, v.Fields, m.FlagName) + } + } +} + +func annotateUnionMembers(cmd *cobra.Command, meta []FlagMeta, unionFlag string) { + for _, m := range meta { + annotateUnionMember(cmd, m.FlagName, unionFlag) + if m.Union == nil { + continue + } + for _, variant := range m.Union.Variants { + annotateUnionMember(cmd, variant.FlagName, unionFlag) + annotateUnionMembers(cmd, variant.Fields, unionFlag) + } + } +} + +func annotateUnionMember(cmd *cobra.Command, name, unionFlag string) { + f := cmd.Flags().Lookup(name) + if f == nil { + return + } + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + f.Annotations[AnnotationUnionMember] = []string{unionFlag} +} + +// buildUnionField handles the complete union parsing flow: +// variant conflict detection, 3-priority parsing, and field assignment. +func buildUnionField(cmd *cobra.Command, root reflect.Value, m FlagMeta) error { + u := m.Union + + // Navigate to the union field on the request struct + unionField, err := navigateToField(root, m.FieldPath) + if err != nil { + return fmt.Errorf("failed to navigate to union field for --%s: %w", m.FlagName, err) + } + + // Determine concrete type (handle optional *UnionType vs UnionType) + unionType := unionField.Type() + isPtr := unionType.Kind() == reflect.Ptr + if isPtr { + unionType = unionType.Elem() + } + + // Check if any union-related flags were set + anySet := FlagChanged(cmd, m.FlagName) + if !anySet && u.Discriminated { + for _, v := range u.Variants { + if HasAnyFlagWithPrefix(cmd, v.FlagName) { + anySet = true + break + } + } + } + + if !anySet { + if u.Optional { + return nil // Leave field as nil + } + if !u.Discriminated { + return &MissingRequiredFlagError{FlagName: m.FlagName} + } + // Discriminated required with nothing set: leave as zero (matches current behavior) + return nil + } + + // Discriminated union: check variant conflicts + if u.Discriminated { + var variantsSet []string + for _, v := range u.Variants { + if HasAnyFlagWithPrefix(cmd, v.FlagName) { + variantsSet = append(variantsSet, v.DiscriminatorValue) + } + } + if len(variantsSet) > 1 { + names := make([]string, len(u.Variants)) + for i, v := range u.Variants { + names[i] = v.DiscriminatorValue + } + return fmt.Errorf("multiple union variants provided for --%s; choose one of: %s", + m.FlagName, strings.Join(names, ", ")) + } + } + + // Priority 1: Top-level JSON flag + if FlagChanged(cmd, m.FlagName) { + jsonStr, _ := GetStringFlag(cmd, m.FlagName) + target := reflect.New(unionType) + if err := utils.UnmarshalJsonFromString(string(applyUnionDefaults([]byte(jsonStr), u)), target.Interface(), ""); err != nil { + return fmt.Errorf("invalid value for --%s: %w", m.FlagName, err) + } + if err := enforceStrictOrWarn(cmd, verifyBodyKeys("--"+m.FlagName, []byte(jsonStr), target, u)); err != nil { + return err + } + setUnionFieldValue(unionField, target, isPtr) + return nil + } + + // Priority 2 & 3: Per-variant flags (discriminated only) + for _, v := range u.Variants { + if !HasAnyFlagWithPrefix(cmd, v.FlagName) { + continue + } + + // Find the variant's Go type via reflection on the union struct + variantStructField, ok := unionType.FieldByName(v.FieldName) + if !ok { + return fmt.Errorf("variant field %q not found on union type %s", v.FieldName, unionType) + } + // Variant fields are always *VariantType pointers + variantType := variantStructField.Type.Elem() + + // Priority 2: Variant-level JSON flag + if FlagChanged(cmd, v.FlagName) { + jsonStr, _ := GetStringFlag(cmd, v.FlagName) + variantPtr := reflect.New(variantType) + if err := utils.UnmarshalJsonFromString(jsonStr, variantPtr.Interface(), ""); err != nil { + return fmt.Errorf("invalid value for --%s: %w", v.FlagName, err) + } + injectDiscriminator(variantPtr.Elem(), u.DiscriminatorKey, v.DiscriminatorValue) + unionVal := assembleUnionValue(unionType, v.FieldName, variantPtr) + setUnionFieldValue(unionField, unionVal, isPtr) + return nil + } + + // Priority 3: Expanded variant fields + if v.CanExpand { + variantVal := reflect.New(variantType).Elem() + injectDiscriminator(variantVal, u.DiscriminatorKey, v.DiscriminatorValue) + + // Build variant fields using the same per-kind builders + for _, fm := range v.Fields { + if err := buildFieldOnValue(cmd, variantVal, fm); err != nil { + return err + } + } + + variantPtr := reflect.New(variantType) + variantPtr.Elem().Set(variantVal) + unionVal := assembleUnionValue(unionType, v.FieldName, variantPtr) + setUnionFieldValue(unionField, unionVal, isPtr) + return nil + } + } + + return nil +} + +// setUnionFieldValue sets a union field on the request struct. +// Handles both value (Shape) and pointer (*Shape) field types. +func setUnionFieldValue(field reflect.Value, val reflect.Value, isPtr bool) { + if isPtr { + // field is *UnionType, val is *UnionType from reflect.New + field.Set(val) + } else if val.Kind() == reflect.Ptr { + // field is UnionType, val is *UnionType → dereference + field.Set(val.Elem()) + } else { + field.Set(val) + } +} + +// injectDiscriminator sets the discriminator field on a variant struct value. +func injectDiscriminator(v reflect.Value, key string, value string) { + if key == "" { + return + } + f := v.FieldByName(key) + if f.IsValid() && f.CanSet() { + f.Set(reflect.ValueOf(value).Convert(f.Type())) + } +} + +// assembleUnionValue creates a new union struct with the given variant pointer set. +func assembleUnionValue(unionType reflect.Type, variantFieldName string, variantPtr reflect.Value) reflect.Value { + unionVal := reflect.New(unionType) + unionElem := unionVal.Elem() + vf := unionElem.FieldByName(variantFieldName) + if vf.IsValid() && vf.CanSet() { + vf.Set(variantPtr) + } + return unionVal +} diff --git a/internal/flagutil/preset.go b/internal/flagutil/preset.go new file mode 100644 index 0000000..5633f35 --- /dev/null +++ b/internal/flagutil/preset.go @@ -0,0 +1,244 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package flagutil + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func MergeOperationDeclaredInputs(cmd *cobra.Command, bodyFlags []string, canonicalBodyFlag string, bodyRequired bool) error { + if cmd == nil { + return nil + } + type declaredInput struct { + name string + key string + value any + } + var inputs []declaredInput + cmd.Flags().VisitAll(func(f *pflag.Flag) { + keys := f.Annotations[AnnotationOpDeclaredInput] + if !f.Changed || len(keys) != 1 || keys[0] == "" { + return + } + value, err := operationDeclaredFlagValue(f) + if err != nil { + inputs = append(inputs, declaredInput{name: f.Name, key: keys[0], value: err}) + return + } + inputs = append(inputs, declaredInput{name: f.Name, key: keys[0], value: value}) + }) + if len(inputs) == 0 { + return nil + } + for _, input := range inputs { + if err, ok := input.value.(error); ok { + return fmt.Errorf("read --%s: %w", input.name, err) + } + } + + suppliedBodyFlag := "" + for _, name := range bodyFlags { + if f := cmd.Flags().Lookup(name); f != nil && f.Changed { + if suppliedBodyFlag != "" && suppliedBodyFlag != name { + return fmt.Errorf("--%s and --%s both supply the request body", suppliedBodyFlag, name) + } + suppliedBodyFlag = name + } + } + if suppliedBodyFlag == "" { + attached, err := AttachStdinBody(cmd, canonicalBodyFlag) + if err != nil { + return err + } + if !attached && bodyRequired { + return nil + } + if attached { + for _, input := range inputs { + if err := MergeInputIntoBody(cmd, "", input.key, "--"+input.name, input.value); err != nil { + return err + } + } + return nil + } + } else { + if err := ResolveBodyFlag(cmd, suppliedBodyFlag); err != nil { + return err + } + for _, input := range inputs { + if err := MergeInputIntoBody(cmd, suppliedBodyFlag, input.key, "--"+input.name, input.value); err != nil { + return err + } + } + return nil + } + if canonicalBodyFlag == "" { + return fmt.Errorf("operation-declared inputs have no whole request-body flag") + } + body := make(map[string]any, len(inputs)) + for _, input := range inputs { + body[input.key] = input.value + } + raw, err := json.Marshal(body) + if err != nil { + return err + } + if err := cmd.Flags().Set(canonicalBodyFlag, string(raw)); err != nil { + return fmt.Errorf("setting --%s: %w", canonicalBodyFlag, err) + } + return nil +} + +func operationDeclaredFlagValue(f *pflag.Flag) (any, error) { + switch f.Value.Type() { + case "bool": + return strconv.ParseBool(f.Value.String()) + case "int", "int8", "int16", "int32", "int64": + return strconv.ParseInt(f.Value.String(), 10, 64) + case "float32", "float64": + return strconv.ParseFloat(f.Value.String(), 64) + default: + return f.Value.String(), nil + } +} + +type PresetMerge struct { + Command string + Variant string + Preset string + Foreign []string // selector keys declared only by other union members + DiscriminatorKey string + DiscriminatorValue string + DiscriminatorAliases []string // every discriminator value (JSON text) selecting the pinned variant + Escape string +} + +type PresetConflictError struct { + Command string + Variant string + Key string + Value string // JSON text of the conflicting discriminator value ("" for a foreign key) + Escape string +} + +func (e *PresetConflictError) Error() string { + var b strings.Builder + fmt.Fprintf(&b, "%q presets the %s request variant, but the supplied body ", e.Command, e.Variant) + if e.Value != "" { + fmt.Fprintf(&b, "sets %q to %s, which selects a different variant", e.Key, e.Value) + } else { + fmt.Fprintf(&b, "names %q, which selects a different variant", e.Key) + } + if e.Escape != "" { + fmt.Fprintf(&b, "; use %q to send that request", e.Escape) + } + return b.String() +} + +func (*PresetConflictError) CLIReason() string { return "CLI_VALIDATION" } + +func MergePresetBody(body string, m PresetMerge) (string, error) { + trimmed := strings.TrimSpace(body) + if trimmed == "" { + trimmed = "{}" + } + if !strings.HasPrefix(trimmed, "{") { + return body, nil + } + var user map[string]json.RawMessage + if err := json.Unmarshal([]byte(trimmed), &user); err != nil { + return body, nil + } + if user == nil { + user = map[string]json.RawMessage{} + } + if err := MergePresetObject(user, m); err != nil { + return "", err + } + + merged, err := json.Marshal(user) + if err != nil { + return "", err + } + return string(merged), nil +} + +func MergePresetObject(user map[string]json.RawMessage, m PresetMerge) error { + + for _, key := range m.Foreign { + if _, present := user[key]; present { + return &PresetConflictError{Command: m.Command, Variant: m.Variant, Key: key, Escape: m.Escape} + } + } + if m.DiscriminatorKey != "" && m.DiscriminatorValue != "" { + if raw, present := user[m.DiscriminatorKey]; present && !m.selectsPinnedVariant(raw) { + return &PresetConflictError{Command: m.Command, Variant: m.Variant, Key: m.DiscriminatorKey, Value: string(bytes.TrimSpace(raw)), Escape: m.Escape} + } + } + + if strings.TrimSpace(m.Preset) != "" { + var preset map[string]json.RawMessage + if err := json.Unmarshal([]byte(m.Preset), &preset); err != nil { + return fmt.Errorf("invalid preset for %q: %w", m.Command, err) + } + for key, value := range preset { + if _, present := user[key]; !present { + user[key] = value + } + } + } + + if m.DiscriminatorKey != "" && m.DiscriminatorValue != "" { + if _, present := user[m.DiscriminatorKey]; !present { + user[m.DiscriminatorKey] = json.RawMessage(m.DiscriminatorValue) + } + } + + return nil +} + +func (m PresetMerge) selectsPinnedVariant(raw json.RawMessage) bool { + if jsonEqual(raw, json.RawMessage(m.DiscriminatorValue)) { + return true + } + for _, alias := range m.DiscriminatorAliases { + if jsonEqual(raw, json.RawMessage(alias)) { + return true + } + } + return false +} + +func jsonEqual(a, b json.RawMessage) bool { + var av, bv any + if err := json.Unmarshal(a, &av); err != nil { + return false + } + if err := json.Unmarshal(b, &bv); err != nil { + return false + } + return reflect.DeepEqual(av, bv) +} diff --git a/internal/interactive/interactive.go b/internal/interactive/interactive.go new file mode 100644 index 0000000..b2acaf7 --- /dev/null +++ b/internal/interactive/interactive.go @@ -0,0 +1,929 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package interactive prompts for declared required inputs in an interactive terminal. +package interactive + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "sort" + "strconv" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" + "github.com/spf13/pflag" +) + +const PromptArgsAnnotation = "speakeasy_prompt_args" + +// ArgSpec declares one promptable positional; SatisfiedBy names flags (no dashes) that replace it. +type ArgSpec struct { + Name string `json:"name"` + Summary string `json:"summary,omitempty"` + Required bool `json:"required,omitempty"` + Variadic bool `json:"variadic,omitempty"` + BodyKey string `json:"body_key,omitempty"` + SatisfiedBy []string `json:"satisfied_by,omitempty"` +} + +type promptArgsWire struct { + Version int `json:"version"` + Args []ArgSpec `json:"args"` +} + +type CommandSpec struct { + Args []ArgSpec +} + +func Declare(cmd *cobra.Command, spec CommandSpec) error { + if cmd == nil { + return fmt.Errorf("declare interactive arguments: nil command") + } + for i, arg := range spec.Args { + if strings.TrimSpace(arg.Name) == "" { + return fmt.Errorf("declare interactive arguments: argument %d has no name", i+1) + } + if arg.Variadic && i != len(spec.Args)-1 { + return fmt.Errorf("declare interactive arguments: variadic argument %q must be last", arg.Name) + } + } + raw, err := json.Marshal(promptArgsWire{Version: 1, Args: spec.Args}) + if err != nil { + return fmt.Errorf("declare interactive arguments: %w", err) + } + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + cmd.Annotations[PromptArgsAnnotation] = string(raw) + if cmd.Annotations["speakeasy_args_help"] == "" && !strings.Contains(cmd.Long, "Arguments:\n") { + parts := make([]string, 0, len(spec.Args)) + for _, arg := range spec.Args { + name := arg.Name + if arg.Required { + name = "<" + name + ">" + } else { + name = "[" + name + "]" + } + if arg.Variadic { + name += "..." + } + parts = append(parts, name) + } + cmd.Annotations["speakeasy_args_help"] = strings.Join(parts, " ") + } + return nil +} + +// PromptField IDs are "arg:" for positionals and "flag:" for flags. +type PromptField struct { + ID string + Name string + Summary string + Kind string + Required bool + Direct bool + Repeatable bool + Options []string + + target promptTarget + flagName string + argIndex int +} + +type promptTarget int + +const ( + promptTargetArg promptTarget = iota + promptTargetFlag +) + +type PromptAnswer struct { + Set bool + Values []string +} + +type Prompter interface { + Prompt(cmd *cobra.Command, fields []PromptField) ([]PromptAnswer, error) +} + +type prompterContextKey struct{} + +func WithPrompter(ctx context.Context, p Prompter) context.Context { + return context.WithValue(ctx, prompterContextKey{}, p) +} + +func injectedPrompter(cmd *cobra.Command) Prompter { + if cmd == nil || cmd.Context() == nil { + return nil + } + p, _ := cmd.Context().Value(prompterContextKey{}).(Prompter) + return p +} + +func init() { + promptFromContext = func(cmd *cobra.Command) bool { + return injectedPrompter(cmd) != nil + } +} + +type commandExecution struct { + originalArgs []string + effectiveArgs []string + deferredArgs bool +} + +func Intercept(root *cobra.Command) { + for _, child := range root.Commands() { + Intercept(child) + } + interceptCommand(root) +} + +func interceptCommand(cmd *cobra.Command) { + if cmd == nil || (cmd.RunE == nil && cmd.Run == nil) { + return + } + spec := commandSpec(cmd) + if len(spec) == 0 && !hasPromptableFlag(cmd) { + return + } + originalArgsValidator := cmd.Args + originalPreRunE, originalPreRun := cmd.PreRunE, cmd.PreRun + originalRunE, originalRun := cmd.RunE, cmd.Run + originalPostRunE, originalPostRun := cmd.PostRunE, cmd.PostRun + state := &commandExecution{} + + if len(spec) > 0 { + cmd.Args = func(runCmd *cobra.Command, args []string) error { + state.originalArgs = append(state.originalArgs[:0], args...) + state.effectiveArgs = append(state.effectiveArgs[:0], args...) + state.deferredArgs = false + err := validateArgs(originalArgsValidator, runCmd, args) + if err == nil || usage.UsageRequested(runCmd) || documentationRequested(runCmd) { + return err + } + // Cobra validates Args before any PersistentPreRunE. + output.InitAgentMode(runCmd) + if !Resolve(runCmd).PromptRequiredInputs() || !hasMissingDeclaredRequiredArg(runCmd, args, spec) { + return err + } + state.deferredArgs = true + return nil + } + } + + cmd.PreRun = nil + cmd.PreRunE = func(runCmd *cobra.Command, args []string) error { + effective := append([]string(nil), state.effectiveArgs...) + if len(state.originalArgs) == 0 && len(args) > 0 { + effective = append([]string(nil), args...) + } + + bodyTookControl := false + if !usage.UsageRequested(runCmd) && !documentationRequested(runCmd) && Resolve(runCmd).PromptRequiredInputs() { + supplied, err := wholeBodySupplied(runCmd) + if err != nil { + return err + } + bodyTookControl = supplied + fields, missing, err := promptPlan(runCmd, effective, spec) + if err != nil { + return err + } + if missing { + prompter := injectedPrompter(runCmd) + if prompter == nil { + prompter = huhPrompter{} + } + answers, err := prompter.Prompt(runCmd, fields) + if err != nil { + return fmt.Errorf("interactive prompt: %w", err) + } + effective, err = applyAnswers(runCmd, effective, fields, answers) + if err != nil { + return err + } + state.effectiveArgs = append(state.effectiveArgs[:0], effective...) + } + } + + if state.deferredArgs && !bodyTookControl { + if err := validateArgs(originalArgsValidator, runCmd, effective); err != nil { + return flagutil.WithCLIValidation(err) + } + } + if originalPreRunE != nil { + return originalPreRunE(runCmd, effective) + } + if originalPreRun != nil { + originalPreRun(runCmd, effective) + } + return nil + } + + if originalRunE != nil || originalRun != nil { + cmd.Run = nil + cmd.RunE = func(runCmd *cobra.Command, args []string) error { + effective := bridgedArgs(state, args) + if originalRunE != nil { + return originalRunE(runCmd, effective) + } + originalRun(runCmd, effective) + return nil + } + } + if originalPostRunE != nil || originalPostRun != nil { + cmd.PostRun = nil + cmd.PostRunE = func(runCmd *cobra.Command, args []string) error { + effective := bridgedArgs(state, args) + if originalPostRunE != nil { + return originalPostRunE(runCmd, effective) + } + originalPostRun(runCmd, effective) + return nil + } + } +} + +func wholeBodySupplied(cmd *cobra.Command) (bool, error) { + bodyFlag := cmd.Annotations[flagutil.AnnotationWholeBodyFlag] + if bodyFlag == "" { + return false, nil + } + f := cmd.Flags().Lookup(bodyFlag) + if f == nil { + return false, nil + } + if f.Changed { + return true, nil + } + data, err := flagutil.ReadStdinBody(cmd, bodyFlag) + if err != nil { + return false, err + } + if len(bytes.TrimSpace(data)) == 0 { + return false, nil + } + value := string(data) + // "@@" is ResolveBodyFlagValue's escape for a literal leading "@". + if strings.HasPrefix(value, "@") { + value = "@" + value + } + if err := cmd.Flags().Set(bodyFlag, value); err != nil { + return false, err + } + return true, nil +} + +func hasPromptableFlag(cmd *cobra.Command) bool { + found := false + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if found || len(f.Annotations[flagutil.AnnotationUnionMember]) > 0 { + return + } + found = annotationTrue(f, flagutil.AnnotationRequired) || annotationTrue(f, cobra.BashCompOneRequiredFlag) || annotationTrue(f, flagutil.AnnotationPromptDirect) + }) + return found +} + +func validateArgs(validator cobra.PositionalArgs, cmd *cobra.Command, args []string) error { + if validator == nil { + return nil + } + return validator(cmd, args) +} + +func bridgedArgs(state *commandExecution, fallback []string) []string { + if state == nil || state.effectiveArgs == nil { + return fallback + } + return state.effectiveArgs +} + +func commandSpec(cmd *cobra.Command) []ArgSpec { + if cmd == nil || cmd.Annotations == nil { + return nil + } + raw := cmd.Annotations[PromptArgsAnnotation] + if raw == "" { + return nil + } + var wire promptArgsWire + if err := json.Unmarshal([]byte(raw), &wire); err != nil || wire.Version != 1 { + return nil + } + return wire.Args +} + +func documentationRequested(cmd *cobra.Command) bool { + requested := false + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if requested || !annotationTrue(f, flagutil.AnnotationDocSurface) || !f.Changed { + return + } + if f.Value.Type() == "bool" { + value, err := strconv.ParseBool(f.Value.String()) + requested = err == nil && value + return + } + requested = true + }) + return requested +} + +func hasMissingDeclaredRequiredArg(cmd *cobra.Command, args []string, spec []ArgSpec) bool { + for i, arg := range spec { + if arg.Required && !argResolved(cmd, args, i, arg) { + return true + } + } + return false +} + +func argResolved(cmd *cobra.Command, args []string, index int, arg ArgSpec) bool { + if index < len(args) { + return true + } + for _, name := range arg.SatisfiedBy { + if alternativeFlagSet(cmd, name) { + return true + } + } + return false +} + +func alternativeFlagSet(cmd *cobra.Command, name string) bool { + f := cmd.Flags().Lookup(name) + if f == nil || !f.Changed { + return false + } + switch f.Value.Type() { + case "bool": + v, err := strconv.ParseBool(f.Value.String()) + return err == nil && v + case "stringArray", "stringSlice": + return strings.TrimSpace(f.Value.String()) != "[]" + default: + return strings.TrimSpace(f.Value.String()) != "" + } +} + +func promptPlan(cmd *cobra.Command, args []string, spec []ArgSpec) ([]PromptField, bool, error) { + var requiredArgs, optionalArgs, requiredFlags, directFlags, optionalFlags []PromptField + missing := false + for i, arg := range spec { + if argResolved(cmd, args, i, arg) { + continue + } + field := PromptField{ + ID: "arg:" + arg.Name, + Name: displayName(arg.Name, "", arg.Required), + Summary: arg.Summary, + Kind: "string", + Required: arg.Required, + Repeatable: arg.Variadic, + target: promptTargetArg, + argIndex: i, + } + if arg.Required { + missing = true + requiredArgs = append(requiredArgs, field) + } else { + optionalArgs = append(optionalArgs, field) + } + } + + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if len(f.Annotations[flagutil.AnnotationUnionMember]) > 0 { + return + } + required := annotationTrue(f, flagutil.AnnotationRequired) || annotationTrue(f, cobra.BashCompOneRequiredFlag) + optional := annotationTrue(f, flagutil.AnnotationPrompt) + direct := annotationTrue(f, flagutil.AnnotationPromptDirect) + if (!required && !optional && !direct) || flagResolved(cmd, f) { + return + } + kind, repeatable := promptKind(f) + options := append([]string(nil), f.Annotations[flagutil.AnnotationPromptValues]...) + if kind == "enum" && len(options) == 0 { + kind = "string" + } + summary := flagSummary(f) + if direct { + summary += " · optional; leave empty for the default" + } + field := PromptField{ + ID: "flag:" + f.Name, + Name: displayName(f.Name, firstAnnotation(f, flagutil.AnnotationPromptLabel), required), + Summary: summary, + Kind: kind, + Required: required, + Direct: direct, + Repeatable: repeatable, + Options: options, + target: promptTargetFlag, + flagName: f.Name, + } + if required { + missing = true + requiredFlags = append(requiredFlags, field) + } else if direct { + directFlags = append(directFlags, field) + } else { + optionalFlags = append(optionalFlags, field) + } + }) + + sortPromptFlags(requiredFlags, cmd) + sortPromptFlags(directFlags, cmd) + sortPromptFlags(optionalFlags, cmd) + if !missing { + return nil, false, nil + } + fields := append(requiredArgs, requiredFlags...) + fields = append(fields, optionalArgs...) + fields = append(fields, directFlags...) + fields = append(fields, optionalFlags...) + return fields, true, nil +} + +func firstAnnotation(f *pflag.Flag, key string) string { + if f == nil || len(f.Annotations[key]) == 0 { + return "" + } + return f.Annotations[key][0] +} + +func sortPromptFlags(fields []PromptField, cmd *cobra.Command) { + sort.SliceStable(fields, func(i, j int) bool { + left := promptOrder(cmd.Flags().Lookup(fields[i].flagName)) + right := promptOrder(cmd.Flags().Lookup(fields[j].flagName)) + if left == right { + return fields[i].flagName < fields[j].flagName + } + return left < right + }) +} + +func promptOrder(f *pflag.Flag) int { + if f == nil || len(f.Annotations[flagutil.AnnotationPromptOrder]) == 0 { + return int(^uint(0) >> 1) + } + v, err := strconv.Atoi(f.Annotations[flagutil.AnnotationPromptOrder][0]) + if err != nil { + return int(^uint(0) >> 1) + } + return v +} + +func annotationTrue(f *pflag.Flag, key string) bool { + values := f.Annotations[key] + return len(values) > 0 && values[0] == "true" +} + +func flagResolved(cmd *cobra.Command, f *pflag.Flag) bool { + if f == nil { + return true + } + if f.Changed || annotationTrue(f, flagutil.AnnotationDefaultResolves) { + return true + } + for _, source := range f.Annotations[flagutil.AnnotationBodyField] { + if body := cmd.Flags().Lookup(source); body != nil && body.Changed { + return true + } + } + if values := f.Annotations[flagutil.AnnotationEnv]; len(values) > 0 { + if _, ok := os.LookupEnv(values[0]); ok { + return true + } + } + if values := f.Annotations[flagutil.AnnotationConfig]; len(values) > 0 { + if config.GetConfigValue(values[0]) != "" { + return true + } + } + return false +} + +func promptKind(f *pflag.Flag) (string, bool) { + if values := f.Annotations[flagutil.AnnotationPromptKind]; len(values) > 0 && values[0] != "" { + kind := values[0] + return kind, kind == "string-array" + } + switch f.Value.Type() { + case "bool": + return "bool", false + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + return "int64", false + case "float32", "float64": + return "float64", false + case "duration": + return "duration", false + case "stringArray", "stringSlice": + return "string-array", true + default: + return "string", false + } +} + +func flagSummary(f *pflag.Flag) string { + if f.Usage == "" { + return "--" + f.Name + } + return f.Usage + " · --" + f.Name +} + +func displayName(name, label string, required bool) string { + if strings.TrimSpace(label) != "" { + name = label + if required { + name += " (required)" + } + return name + } + if idx := strings.LastIndex(name, "."); idx >= 0 { + name = name[idx+1:] + } + parts := strings.Split(name, "-") + for i, part := range parts { + if part != "" { + parts[i] = strings.ToUpper(part[:1]) + part[1:] + } + } + name = strings.Join(parts, " ") + if required { + name += " (required)" + } + return name +} + +func applyAnswers(cmd *cobra.Command, args []string, fields []PromptField, answers []PromptAnswer) ([]string, error) { + if len(answers) != len(fields) { + return nil, fmt.Errorf("interactive prompt returned %d answer(s) for %d field(s)", len(answers), len(fields)) + } + effective := append([]string(nil), args...) + for i, field := range fields { + answer := answers[i] + answerHasValue := false + for _, value := range answer.Values { + if strings.TrimSpace(value) != "" { + answerHasValue = true + break + } + } + if !answer.Set || !answerHasValue { + if field.Required { + return nil, fmt.Errorf("interactive prompt left required input %q unset", field.ID) + } + continue + } + if field.target == promptTargetArg { + for _, value := range answer.Values { + if value != "" { + effective = append(effective, value) + } + } + continue + } + values := answer.Values + if !field.Repeatable && len(values) > 1 { + values = values[:1] + } + for _, value := range values { + if err := cmd.Flags().Set(field.flagName, value); err != nil { + return nil, fmt.Errorf("setting flag %q: %w", field.flagName, err) + } + } + } + return effective, nil +} + +// formWidth returns the terminal width for sizing huh forms, with a sensible default. +func formWidth() int { + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || width <= 0 { + width = 80 + } + return width +} + +// formTheme builds the interactive prompt theme. +func formTheme() *huh.Theme { + t := *huh.ThemeBase() + + accent := lipgloss.Color("#38BDF8") + dimmed := lipgloss.Color("#64748B") + subtle := lipgloss.Color("#475569") + errColor := lipgloss.Color("#F87171") + greenColor := lipgloss.Color("#4ADE80") + + t.Focused.Base = t.Focused.Base.BorderLeft(true).BorderStyle(lipgloss.ThickBorder()).BorderForeground(accent).PaddingLeft(1) + t.Focused.Title = t.Focused.Title.Foreground(accent).Bold(true) + t.Focused.Description = t.Focused.Description.Foreground(dimmed).Italic(true) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errColor) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errColor) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accent).SetString("> ") + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(accent).Bold(true) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(greenColor).SetString("✓ ").Bold(true) + t.Focused.UnselectedPrefix = lipgloss.NewStyle().SetString(" ") + t.Focused.FocusedButton = t.Focused.FocusedButton.Background(accent).Foreground(lipgloss.Color("#FFFFFF")) + t.Focused.BlurredButton = t.Focused.BlurredButton.Background(subtle) + t.Focused.Next = t.Focused.FocusedButton + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(accent) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(subtle).Italic(true) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accent) + + t.Blurred.Base = t.Blurred.Base.BorderLeft(true).BorderStyle(lipgloss.ThickBorder()).BorderForeground(subtle).PaddingLeft(1) + t.Blurred.Title = t.Blurred.Title.Foreground(dimmed) + t.Blurred.Description = t.Blurred.Description.Foreground(subtle).Italic(true) + t.Blurred.TextInput.Text = t.Blurred.TextInput.Text.Foreground(dimmed) + t.Blurred.TextInput.Placeholder = t.Blurred.TextInput.Placeholder.Foreground(subtle).Italic(true) + t.Blurred.SelectedOption = t.Blurred.SelectedOption.Foreground(dimmed) + t.Blurred.SelectSelector = t.Blurred.SelectSelector.Foreground(dimmed) + t.Blurred.SelectedPrefix = lipgloss.NewStyle().Foreground(dimmed).SetString("✓ ") + t.Blurred.UnselectedPrefix = lipgloss.NewStyle().SetString(" ") + return &t +} + +func printCommandHeader(cmd *cobra.Command, totalRequired, totalOptional int) { + accent := lipgloss.Color("#38BDF8") + dimmed := lipgloss.Color("#64748B") + titleStyle := lipgloss.NewStyle().Foreground(accent).Bold(true) + descStyle := lipgloss.NewStyle().Foreground(dimmed).Italic(true) + infoStyle := lipgloss.NewStyle().Foreground(dimmed) + + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, titleStyle.Render(cmd.CommandPath())) + if cmd.Long != "" { + fmt.Fprintln(os.Stderr, descStyle.Render(strings.SplitN(cmd.Long, "\n", 2)[0])) + } else if cmd.Short != "" { + fmt.Fprintln(os.Stderr, descStyle.Render(cmd.Short)) + } + var parts []string + if totalRequired > 0 { + parts = append(parts, fmt.Sprintf("%d required", totalRequired)) + } + if totalOptional > 0 { + parts = append(parts, fmt.Sprintf("%d optional", totalOptional)) + } + if len(parts) > 0 { + fmt.Fprintln(os.Stderr, infoStyle.Render(strings.Join(parts, ", ")+" field(s)")) + } + fmt.Fprintln(os.Stderr) +} + +type huhPrompter struct{} + +func (huhPrompter) Prompt(cmd *cobra.Command, fields []PromptField) ([]PromptAnswer, error) { + answers := make([]PromptAnswer, len(fields)) + required := make([]int, 0, len(fields)) + optionalArgs := make([]int, 0, len(fields)) + direct := make([]int, 0, len(fields)) + optionalFlags := make([]int, 0, len(fields)) + for i, field := range fields { + if field.Required { + required = append(required, i) + } else if strings.HasPrefix(field.ID, "arg:") { + optionalArgs = append(optionalArgs, i) + } else if field.Direct { + direct = append(direct, i) + } else { + optionalFlags = append(optionalFlags, i) + } + } + printCommandHeader(cmd, len(required), len(optionalArgs)+len(direct)+len(optionalFlags)) + theme, width := formTheme(), formWidth() + for _, index := range required { + answer, err := runPromptField(fields[index], theme, width) + if err != nil { + return nil, err + } + answers[index] = answer + } + for _, index := range optionalArgs { + field := fields[index] + field.Summary = optionalArgDescription(field.Summary) + answer, err := runPromptField(field, theme, width) + if err != nil { + return nil, err + } + answers[index] = answer + } + for _, index := range direct { + answer, err := runPromptField(fields[index], theme, width) + if err != nil { + return nil, err + } + answers[index] = answer + } + if len(optionalFlags) == 0 { + return answers, nil + } + fillOptional := false + names := make([]string, 0, len(optionalFlags)) + for _, index := range optionalFlags { + names = append(names, fields[index].Name) + } + form := huh.NewForm(huh.NewGroup(huh.NewConfirm(). + Title(fmt.Sprintf("Fill in %d optional field(s)?", len(optionalFlags))). + Description(strings.Join(names, ", ")).Value(&fillOptional))). + WithWidth(width).WithTheme(theme).WithShowHelp(false) + if err := form.Run(); err != nil { + return nil, err + } + if fillOptional { + for _, index := range optionalFlags { + answer, err := runPromptField(fields[index], theme, width) + if err != nil { + return nil, err + } + answers[index] = answer + } + } + return answers, nil +} + +func optionalArgDescription(summary string) string { + if strings.TrimSpace(summary) == "" { + return "Optional, leave empty to skip" + } + return summary + " · optional, leave empty to skip" +} + +func runPromptField(field PromptField, theme *huh.Theme, width int) (PromptAnswer, error) { + if field.Repeatable && field.target == promptTargetFlag { + return runRepeatableFlagPrompt(field, theme, width) + } + + var answer PromptAnswer + var formField huh.Field + switch field.Kind { + case "bool": + if field.Required { + value := false + formField = huh.NewConfirm().Title(field.Name).Description(field.Summary).Value(&value) + answer.Set = true + answer.Values = []string{strconv.FormatBool(value)} + form := huh.NewForm(huh.NewGroup(formField)).WithWidth(width).WithTheme(theme).WithShowHelp(false) + if err := form.Run(); err != nil { + return PromptAnswer{}, err + } + answer.Values[0] = strconv.FormatBool(value) + return answer, nil + } + value := "" + formField = huh.NewSelect[string]().Title(field.Name).Description(field.Summary).Options( + huh.NewOption("Yes", "true"), huh.NewOption("No", "false"), huh.NewOption("Skip (use default)", ""), + ).Value(&value) + form := huh.NewForm(huh.NewGroup(formField)).WithWidth(width).WithTheme(theme).WithShowHelp(false) + if err := form.Run(); err != nil { + return PromptAnswer{}, err + } + return PromptAnswer{Set: value != "", Values: []string{value}}, nil + + case "enum": + value := "" + options := make([]huh.Option[string], 0, len(field.Options)+1) + if !field.Required { + options = append(options, huh.NewOption[string]("Skip (use default)", "")) + } + for _, option := range field.Options { + options = append(options, huh.NewOption(option, option)) + } + formField = huh.NewSelect[string]().Title(field.Name).Description(field.Summary).Options(options...).Value(&value) + form := huh.NewForm(huh.NewGroup(formField)).WithWidth(width).WithTheme(theme).WithShowHelp(false) + if err := form.Run(); err != nil { + return PromptAnswer{}, err + } + return PromptAnswer{Set: value != "", Values: []string{value}}, nil + } + + value := "" + description := field.Summary + var validate func(string) error + if field.Required { + validate = func(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("required") + } + return nil + } + } + switch field.Kind { + case "json": + formField = huh.NewText().Title(field.Name).Description(description + " (JSON)").Value(&value) + if validate != nil { + formField = formField.(*huh.Text).Validate(validate) + } + case "int64": + formField = huh.NewInput().Title(field.Name).Description(description).Value(&value).Validate(func(value string) error { + if strings.TrimSpace(value) == "" { + if validate != nil { + return validate(value) + } + return nil + } + if _, err := strconv.ParseInt(value, 10, 64); err != nil { + return fmt.Errorf("must be an integer") + } + return nil + }) + case "float64": + formField = huh.NewInput().Title(field.Name).Description(description).Value(&value).Validate(func(value string) error { + if strings.TrimSpace(value) == "" { + if validate != nil { + return validate(value) + } + return nil + } + if _, err := strconv.ParseFloat(value, 64); err != nil { + return fmt.Errorf("must be a number") + } + return nil + }) + case "duration": + formField = huh.NewInput().Title(field.Name).Description(description).Value(&value).Validate(func(value string) error { + if strings.TrimSpace(value) == "" { + if validate != nil { + return validate(value) + } + return nil + } + if _, err := time.ParseDuration(value); err != nil { + return fmt.Errorf("must be a duration") + } + return nil + }) + default: + input := huh.NewInput().Title(field.Name).Description(description).Value(&value) + if validate != nil { + input = input.Validate(validate) + } + formField = input + } + form := huh.NewForm(huh.NewGroup(formField)).WithWidth(width).WithTheme(theme).WithShowHelp(false) + if err := form.Run(); err != nil { + return PromptAnswer{}, err + } + if strings.TrimSpace(value) == "" { + return PromptAnswer{}, nil + } + return PromptAnswer{Set: true, Values: []string{value}}, nil +} + +func runRepeatableFlagPrompt(field PromptField, theme *huh.Theme, width int) (PromptAnswer, error) { + values := make([]string, 0, 1) + for { + value := "" + title := field.Name + description := field.Summary + " (one value per entry)" + if len(values) > 0 { + title = "Add another " + displayName(strings.TrimSuffix(field.Name, " (required)"), "", false) + "?" + description = "Leave empty to finish." + } + input := huh.NewInput().Title(title).Description(description).Value(&value) + if field.Required && len(values) == 0 { + input = input.Validate(func(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("required") + } + return nil + }) + } + form := huh.NewForm(huh.NewGroup(input)).WithWidth(width).WithTheme(theme).WithShowHelp(false) + if err := form.Run(); err != nil { + return PromptAnswer{}, err + } + if strings.TrimSpace(value) == "" { + break + } + values = append(values, value) + } + return PromptAnswer{Set: len(values) > 0, Values: values}, nil +} diff --git a/internal/interactive/policy.go b/internal/interactive/policy.go new file mode 100644 index 0000000..31c6ff1 --- /dev/null +++ b/internal/interactive/policy.go @@ -0,0 +1,119 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package interactive centralizes the activation policy for prompt, form, and explorer surfaces. +package interactive + +import ( + "fmt" + "os" + + "github.com/google-gemini/gemini-api-cli/internal/output" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +type FormMode int + +const ( + FormOff FormMode = iota + FormTUI + FormAccessible +) + +type Decision struct { + requested bool + terminalPair bool + hasInjectedPrompter bool + explicitNoInteractive bool + explicitInteractiveFalse bool +} + +var promptFromContext = func(*cobra.Command) bool { return false } + +func Resolve(cmd *cobra.Command) Decision { + if cmd == nil { + return Decision{} + } + + output.InitAgentMode(cmd) + interactive, interactiveChanged := rootBoolFlag(cmd, "interactive") + noInteractive, _ := rootBoolFlag(cmd, "no-interactive") + + d := Decision{ + requested: interactive, + terminalPair: terminalPair(), + hasInjectedPrompter: promptFromContext(cmd), + explicitNoInteractive: noInteractive, + explicitInteractiveFalse: interactiveChanged && !interactive, + } + if noInteractive || output.IsAgentMode() { + d.requested = false + } + return d +} + +func IsInteractive(cmd *cobra.Command) bool { + return Resolve(cmd).PromptRequiredInputs() +} + +func (d Decision) PromptRequiredInputs() bool { + return d.requested && (d.terminalPair || d.hasInjectedPrompter) +} + +func (d Decision) FormMode() FormMode { + if !d.requested { + return FormOff + } + if d.terminalPair { + return FormTUI + } + return FormAccessible +} + +func (d Decision) AutoExplore() bool { + return d.requested && d.terminalPair +} + +func (d Decision) ValidateDirectExplore() error { + if d.explicitNoInteractive || d.explicitInteractiveFalse { + return fmt.Errorf("explore conflicts with --no-interactive/--interactive=false") + } + if !d.terminalPair { + return fmt.Errorf("explore requires an interactive terminal (stdin and stdout must be a TTY)") + } + return nil +} + +func terminalPair() bool { + return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) +} + +func rootBoolFlag(cmd *cobra.Command, name string) (bool, bool) { + if cmd == nil || cmd.Root() == nil { + return false, false + } + flags := cmd.Root().PersistentFlags() + f := flags.Lookup(name) + if f == nil { + return false, false + } + value, err := flags.GetBool(name) + if err != nil { + return false, f.Changed + } + return value, f.Changed +} diff --git a/internal/output/agentmode.go b/internal/output/agentmode.go new file mode 100644 index 0000000..9070fdd --- /dev/null +++ b/internal/output/agentmode.go @@ -0,0 +1,169 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "encoding/json" + "fmt" + "strconv" + "sync/atomic" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + agentMode atomic.Bool +) + +// InitAgentMode applies the explicit --agent-mode setting. +func InitAgentMode(cmd *cobra.Command) { + if flagVal, changed := flagutil.GetBoolFlag(cmd, "agent-mode"); changed { + agentMode.Store(flagVal) + return + } + agentMode.Store(false) +} + +// IsAgentMode returns true when agent mode is active. +func IsAgentMode() bool { + return agentMode.Load() +} + +// ResetAgentMode resets agent mode state for testing. +func ResetAgentMode() { + agentMode.Store(false) +} + +func shouldRenderStructuredError(format, jqExpr string) bool { + if IsAgentMode() || format == "json" || jqExpr != "" { + return true + } + return format == "toon" +} + +func shouldClassifyError(_, _ string) bool { + if IsAgentMode() { + return true + } + return true +} + +func AgentModeError(cmd *cobra.Command, message string, hints []string) error { + err := withErrorOrigin( + withCLIHints(WithCLIReason(fmt.Errorf("%s", message), ReasonCLIValidation), hints), + ErrorOriginCLI, + ) + classification := Classify(cmd, err) + return renderClassifiedError(cmd, err, classification, true) +} + +// Cobra aborts flag parsing at the first unknown command or flag, before these flags are seen. +var preparsedRendering struct { + outputFormat string + jq string +} + +// ResetPreparsedRendering clears the rendering flags captured by +// PreparseRenderingFlags. Every parse starts from a reset so a later +// ExecuteRoot in the same process never inherits an earlier invocation's +// --output-format or --jq; the explore hand-off forwards the outer +// invocation's rendering flags in argv (see ExplorerHandoffArgs), so they +// are captured again there. Test harnesses that drive output helpers on +// hand-built commands without re-parsing call this directly. +func ResetPreparsedRendering() { + preparsedRendering.outputFormat = "" + preparsedRendering.jq = "" +} + +func PreparseRenderingFlags(target *cobra.Command, args []string) { + ResetPreparsedRendering() + fs := pflag.NewFlagSet("rendering", pflag.ContinueOnError) + fs.ParseErrorsWhitelist.UnknownFlags = true + fs.SetOutput(nopWriter{}) + fs.Usage = func() {} + mirror := func(name, shorthand, noOptDefVal string) { + if fs.Lookup(name) != nil { + return + } + if shorthand != "" && fs.ShorthandLookup(shorthand) != nil { + shorthand = "" + } + fs.StringP(name, shorthand, "", "") + fs.Lookup(name).NoOptDefVal = noOptDefVal + } + mirror("agent-mode", "", "true") + mirror("output-format", "o", "") + mirror("jq", "q", "") + if target != nil { + target.InheritedFlags() + target.Flags().VisitAll(func(f *pflag.Flag) { + mirror(f.Name, f.Shorthand, f.NoOptDefVal) + }) + } + _ = fs.Parse(args) + if fs.Changed("agent-mode") { + if enabled, err := strconv.ParseBool(fs.Lookup("agent-mode").Value.String()); err == nil { + agentMode.Store(enabled) + } + } + if fs.Changed("output-format") { + preparsedRendering.outputFormat = fs.Lookup("output-format").Value.String() + } + if fs.Changed("jq") { + preparsedRendering.jq = fs.Lookup("jq").Value.String() + } +} + +type nopWriter struct{} + +func (nopWriter) Write(p []byte) (int, error) { return len(p), nil } + +func CLIError(cmd *cobra.Command, err error) error { + if err == nil || IsRendered(err) { + return err + } + jqExpr, changed := flagutil.GetStringFlag(cmd, "jq") + if !changed || jqExpr == "" { + jqExpr = preparsedRendering.jq + } + format := resolveOutputFormat(cmd) + classification := Classify(cmd, withErrorOrigin(err, ErrorOriginCLI)) + if shouldClassifyError(format, jqExpr) { + return renderClassifiedError(cmd, err, classification, shouldRenderStructuredError(format, jqExpr)) + } + if !shouldRenderStructuredError(format, jqExpr) { + return retainClassification(err, classification) + } + + envelope := map[string]interface{}{ + "error": err.Error(), + "exit_code": ExitCodeFor(classification), + } + mergeMachineErrorFields(envelope, err) + if hints := errorCLIHints(err); len(hints) > 0 { + envelope["hints"] = hints + } + jsonData, marshalErr := json.MarshalIndent(envelope, "", " ") + if marshalErr != nil { + return retainClassification(err, classification) + } + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + printJSON(cmd.ErrOrStderr(), jsonData, ShouldColorize(colorFlag)) + return markRendered(err, classification) +} diff --git a/internal/output/artifact.go b/internal/output/artifact.go new file mode 100644 index 0000000..55f2aa0 --- /dev/null +++ b/internal/output/artifact.go @@ -0,0 +1,470 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package output — artifact support (media content written to files). +package output + +import ( + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" +) + +const artifactAnnotation = "speakeasy_artifact" + +type artifactSegment struct { + Field string `json:"field,omitempty"` + Wild bool `json:"wild,omitempty"` +} + +type artifactConfig struct { + Pointer []artifactSegment `json:"pointer"` + Kind string `json:"kind"` // image | audio | video + DefaultPath string `json:"defaultPath"` + // Content-block member names, declared in the generation manifest and + // carried here only when they differ from the defaults applied in + // activeArtifact. + TypeField string `json:"typeField,omitempty"` + DataField string `json:"dataField,omitempty"` + MimeTypeField string `json:"mimeTypeField,omitempty"` + URIField string `json:"uriField,omitempty"` + // Response-root identity member names used to enrich the reported + // envelope and the not-ready diagnostic; TerminalStatus is the status + // value at which content is expected. + IDField string `json:"idField,omitempty"` + StatusField string `json:"statusField,omitempty"` + TerminalStatus string `json:"terminalStatus,omitempty"` +} + +func WantsRawResponse(cmd *cobra.Command) bool { + if cmd == nil { + return false + } + if _, declared := cmd.Annotations[artifactAnnotation]; !declared { + return false + } + rawResponse, _ := flagutil.GetBoolFlag(cmd, "raw-response") + return rawResponse +} + +func activeArtifact(cmd *cobra.Command) *artifactConfig { + if cmd == nil { + return nil + } + raw, ok := cmd.Annotations[artifactAnnotation] + if !ok || raw == "" { + return nil + } + if WantsRawResponse(cmd) { + return nil + } + if dryRun, _ := flagutil.GetBoolFlag(cmd, "dry-run"); dryRun { + return nil + } + var cfg artifactConfig + if err := json.Unmarshal([]byte(raw), &cfg); err != nil { + return nil + } + if cfg.Kind == "" || cfg.DefaultPath == "" || len(cfg.Pointer) == 0 { + return nil + } + if cfg.TypeField == "" { + cfg.TypeField = "type" + } + if cfg.DataField == "" { + cfg.DataField = "data" + } + if cfg.MimeTypeField == "" { + cfg.MimeTypeField = "mime_type" + } + if cfg.URIField == "" { + cfg.URIField = "uri" + } + if cfg.IDField == "" { + cfg.IDField = "id" + } + if cfg.StatusField == "" { + cfg.StatusField = "status" + } + if cfg.TerminalStatus == "" { + cfg.TerminalStatus = "completed" + } + return &cfg +} + +func artifactResult(cmd *cobra.Command, res interface{}, cfg *artifactConfig) error { + var parsed interface{} + rawBody, rawErr := tryReadRawBody(res) + if rawErr != nil { + return rawErr + } + if len(rawBody) > 0 { + if err := json.Unmarshal(rawBody, &parsed); err != nil { + return fmt.Errorf("cannot extract the %s: the response was not valid JSON: %w", cfg.Kind, err) + } + } else { + content := extractResultContent(res) + if content == nil { + return artifactMissingError(cfg, nil) + } + data, err := marshalJSON(content) + if err != nil { + return err + } + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("cannot extract the %s: %w", cfg.Kind, err) + } + } + + item := findArtifactItem(parsed, cfg) + if item == nil { + return artifactMissingError(cfg, parsed) + } + b64, _ := item[cfg.DataField].(string) + if b64 == "" { + if uri, _ := item[cfg.URIField].(string); uri != "" { + return fmt.Errorf("the %s content was delivered by URI, which this CLI cannot download yet; fetch it directly: %s", cfg.Kind, uri) + } + return artifactMissingError(cfg, parsed) + } + payload, err := decodeArtifactBase64(b64) + if err != nil { + return fmt.Errorf("the %s content is not valid base64: %w", cfg.Kind, err) + } + if len(payload) == 0 { + return fmt.Errorf("the %s content decoded to zero bytes; not writing a file", cfg.Kind) + } + + mimeType, _ := item[cfg.MimeTypeField].(string) + outFlag, _ := flagutil.GetStringFlag(cmd, "out") + finalPath, extensionRewritten, err := writeArtifact(outFlag, payload, mimeType, cfg) + if err != nil { + return err + } + if extensionRewritten && !IsMachineMode(cmd) { + fmt.Fprintf(cmd.ErrOrStderr(), "Note: wrote %s as %s (requested %s)\n", mimeType, filepath.Base(finalPath), filepath.Base(outFlag)) + } + absPath, err := filepath.Abs(finalPath) + if err != nil { + absPath = finalPath + } + written, err := os.Stat(absPath) + if err != nil { + return fmt.Errorf("verify written %s %s: %w", cfg.Kind, absPath, err) + } + if written.Size() <= 0 { + return fmt.Errorf("the written %s artifact is empty: %s", cfg.Kind, absPath) + } + + if !IsMachineMode(cmd) { + mimeNote := "" + if mimeType != "" { + mimeNote = ", " + mimeType + } + fmt.Fprintf(cmd.ErrOrStderr(), "Wrote %s to %s (%d bytes%s)\n", cfg.Kind, absPath, written.Size(), mimeNote) + } + + envelope := map[string]interface{}{ + "path": absPath, + "kind": cfg.Kind, + "size_bytes": written.Size(), + } + if mimeType != "" { + envelope["mime_type"] = mimeType + } + if m, ok := parsed.(map[string]interface{}); ok { + if id, ok := m[cfg.IDField].(string); ok && id != "" { + envelope["response_id"] = id + } + if status, ok := m[cfg.StatusField].(string); ok && status != "" { + envelope["status"] = status + } + } + return renderArtifactEnvelope(cmd, absPath, envelope) +} + +func renderArtifactEnvelope(cmd *cobra.Command, absPath string, envelope map[string]interface{}) error { + return renderPathEnvelope(cmd, absPath, envelope) +} + +func artifactCandidates(node interface{}, segs []artifactSegment) []interface{} { + if len(segs) == 0 { + return []interface{}{node} + } + seg := segs[0] + if seg.Wild { + arr, ok := node.([]interface{}) + if !ok { + return nil + } + var out []interface{} + for _, item := range arr { + out = append(out, artifactCandidates(item, segs[1:])...) + } + return out + } + m, ok := node.(map[string]interface{}) + if !ok { + return nil + } + child, ok := m[seg.Field] + if !ok { + return nil + } + return artifactCandidates(child, segs[1:]) +} + +func findArtifactItem(parsed interface{}, cfg *artifactConfig) map[string]interface{} { + for _, candidate := range artifactCandidates(parsed, cfg.Pointer) { + m, ok := candidate.(map[string]interface{}) + if !ok { + continue + } + if t, _ := m[cfg.TypeField].(string); t == cfg.Kind { + return m + } + } + return nil +} + +func artifactMissingError(cfg *artifactConfig, parsed interface{}) error { + msg := fmt.Sprintf("the response contained no %s content to write", cfg.Kind) + if m, ok := parsed.(map[string]interface{}); ok { + status, _ := m[cfg.StatusField].(string) + id, _ := m[cfg.IDField].(string) + if status != "" && status != cfg.TerminalStatus { + msg += fmt.Sprintf(" (status %q", status) + if id != "" { + msg += fmt.Sprintf(", id %q", id) + } + msg += " — the result may not be ready yet)" + } + } + return fmt.Errorf("%s", msg) +} + +func decodeArtifactBase64(s string) ([]byte, error) { + s = strings.TrimSpace(s) + var firstErr error + for _, enc := range []*base64.Encoding{ + base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding, + } { + decoded, err := enc.DecodeString(s) + if err == nil { + return decoded, nil + } + if firstErr == nil { + firstErr = err + } + } + return nil, firstErr +} + +func writeArtifact(outFlag string, payload []byte, mimeType string, cfg *artifactConfig) (string, bool, error) { + ext := artifactExtension(cfg.Kind, mimeType) + + if outFlag == "" { + path, err := writeArtifactDefault("", payload, ext, cfg) + return path, false, err + } + + info, statErr := os.Stat(outFlag) + endsWithSep := strings.HasSuffix(outFlag, "/") || strings.HasSuffix(outFlag, string(os.PathSeparator)) + if (statErr == nil && info.IsDir()) || (statErr != nil && endsWithSep) { + if statErr != nil { + if err := os.MkdirAll(outFlag, 0o755); err != nil { + return "", false, fmt.Errorf("cannot create output directory %s: %w", outFlag, err) + } + } + path, err := writeArtifactDefault(outFlag, payload, ext, cfg) + return path, false, err + } + + finalPath := artifactRewriteExtension(outFlag, ext) + if dir := filepath.Dir(finalPath); dir != "." { + if err := os.MkdirAll(dir, 0o755); err != nil { + return "", false, fmt.Errorf("cannot create output directory %s: %w", dir, err) + } + } + if err := artifactAtomicReplace(finalPath, payload); err != nil { + return "", false, err + } + rewritten := filepath.Ext(outFlag) != "" && finalPath != outFlag + return finalPath, rewritten, nil +} + +func artifactRewriteExtension(path, ext string) string { + if ext == "" { + return path + } + currentExt := filepath.Ext(path) + current := strings.TrimPrefix(strings.ToLower(currentExt), ".") + if current == ext || (current == "jpeg" && ext == "jpg") || (current == "jpg" && ext == "jpeg") { + return path + } + if current == "" { + return path + "." + ext + } + return strings.TrimSuffix(path, currentExt) + "." + ext +} + +func writeArtifactDefault(dir string, payload []byte, ext string, cfg *artifactConfig) (string, error) { + if ext == "" { + ext = artifactKindFallbackExt(cfg.Kind) + } + name := cfg.DefaultPath + name = strings.ReplaceAll(name, "{timestamp}", time.Now().Format("20060102-150405")) + name = strings.ReplaceAll(name, "{rand}", artifactRandSuffix()) + name = strings.ReplaceAll(name, "{ext}", ext) + base := filepath.Join(dir, filepath.FromSlash(name)) + if parent := filepath.Dir(base); parent != "." { + if err := os.MkdirAll(parent, 0o755); err != nil { + return "", fmt.Errorf("cannot create output directory %s: %w", parent, err) + } + } + + extPart := filepath.Ext(base) + stem := strings.TrimSuffix(base, extPart) + for i := 0; i < 1000; i++ { + candidate := base + if i > 0 { + candidate = fmt.Sprintf("%s-%d%s", stem, i+1, extPart) + } + f, err := os.OpenFile(candidate, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + if os.IsExist(err) { + continue + } + return "", fmt.Errorf("cannot create %s: %w", candidate, err) + } + if _, err := f.Write(payload); err != nil { + f.Close() + os.Remove(candidate) + return "", fmt.Errorf("cannot write %s: %w", candidate, err) + } + if err := f.Close(); err != nil { + os.Remove(candidate) + return "", fmt.Errorf("cannot write %s: %w", candidate, err) + } + return candidate, nil + } + return "", fmt.Errorf("could not find a collision-free filename near %s", base) +} + +func artifactAtomicReplace(path string, payload []byte) error { + dir := filepath.Dir(path) + tmp, err := os.CreateTemp(dir, ".artifact-*") + if err != nil { + return fmt.Errorf("cannot create %s: %w", path, err) + } + tmpName := tmp.Name() + cleanup := func() { os.Remove(tmpName) } + if _, err := tmp.Write(payload); err != nil { + tmp.Close() + cleanup() + return fmt.Errorf("cannot write %s: %w", path, err) + } + // chmod through the descriptor, not the path (symlink swap). + if err := tmp.Chmod(0o644); err != nil { + tmp.Close() + cleanup() + return fmt.Errorf("cannot write %s: %w", path, err) + } + if err := tmp.Close(); err != nil { + cleanup() + return fmt.Errorf("cannot write %s: %w", path, err) + } + if err := os.Rename(tmpName, path); err != nil { + cleanup() + return fmt.Errorf("cannot write %s: %w", path, err) + } + return nil +} + +func artifactExtension(kind, mimeType string) string { + base := strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0])) + if base == "" { + return "" + } + top, sub, ok := strings.Cut(base, "/") + if !ok || top != kind { + return "" + } + switch base { + case "image/png": + return "png" + case "image/jpeg", "image/jpg": + return "jpg" + case "audio/wav", "audio/x-wav", "audio/l16", "audio/pcm": + return "wav" + case "audio/mpeg", "audio/mp3": + return "mp3" + case "audio/ogg", "audio/vorbis": + return "ogg" + case "audio/mp4", "audio/m4a": + return "m4a" + case "video/mp4": + return "mp4" + case "video/webm": + return "webm" + case "video/quicktime": + return "mov" + case "video/mpeg": + return "mpg" + } + var b strings.Builder + for _, r := range sub { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { + b.WriteRune(r) + } + } + return b.String() +} + +func artifactKindFallbackExt(kind string) string { + switch kind { + case "audio": + return "wav" + case "video": + return "mp4" + default: + return "jpg" + } +} + +func artifactRandSuffix() string { + var buf [8]byte + if _, err := rand.Read(buf[:]); err != nil { + return fmt.Sprintf("%06d", time.Now().Nanosecond()%1000000) + } + v := binary.BigEndian.Uint64(buf[:]) + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789" + out := make([]byte, 6) + for i := range out { + out[i] = alphabet[v%36] + v /= 36 + } + return string(out) +} diff --git a/internal/output/async.go b/internal/output/async.go new file mode 100644 index 0000000..07e53a0 --- /dev/null +++ b/internal/output/async.go @@ -0,0 +1,441 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" +) + +const asyncAnnotation = "speakeasy_async" + +type asyncConfig struct { + IDPointer string `json:"idPointer"` + StatePointer string `json:"statePointer"` + States map[string]string `json:"states"` + Interval string `json:"interval"` + Backoff float64 `json:"backoff"` + MaxInterval string `json:"maxInterval"` + Timeout string `json:"timeout"` + Command string `json:"command"` + Resume string `json:"resume"` + Params []AsyncParameter `json:"params"` + // Declared failure-detail bindings: the poll-response root member + // holding the failure detail object and the string member inside it + // carrying the human message. Absent members mean the defaults + // ("error"/"message"), applied in activeAsync. + ErrorField string `json:"errorField,omitempty"` + ErrorMessageField string `json:"errorMessageField,omitempty"` +} + +// AsyncParameter is a linked scalar preset applied to every poll request. +type AsyncParameter struct { + In string `json:"in"` + Name string `json:"name"` + Value interface{} `json:"value"` +} + +type asyncSnapshot struct { + raw []byte + parsed interface{} + headers http.Header +} + +type asyncHTTPMeta struct { + Response *http.Response +} + +type asyncResultEnvelope struct { + HTTPMeta asyncHTTPMeta + Result interface{} +} + +type AsyncPollFunc func(context.Context) (interface{}, error) + +// AsyncPollFactory binds the response handle and prepares the typed SDK call. +type AsyncPollFactory func(*cobra.Command, string, []AsyncParameter) (AsyncPollFunc, error) + +type asyncRuntimeError struct { + reason string + message string + id string + resume string + payload interface{} + // errorField is the declared poll-response member holding the failure + // detail (the envelope unwraps it when present); empty means the + // default "error". + errorField string +} + +func (e *asyncRuntimeError) Error() string { return e.message } + +func (e *asyncRuntimeError) CLIReason() string { return e.reason } + +func (e *asyncRuntimeError) MachineErrorFields() map[string]interface{} { + fields := map[string]interface{}{ + "id": e.id, + "resume": e.resume, + } + if e.payload != nil { + errorMember := e.errorField + if errorMember == "" { + errorMember = "error" + } + errorPayload := e.payload + if object, ok := e.payload.(map[string]interface{}); ok { + if member, exists := object[errorMember]; exists { + errorPayload = member + } + } + fields["error"] = errorPayload + } + return fields +} + +type asyncTimings struct { + interval time.Duration + maxInterval time.Duration + timeout time.Duration +} + +// AsyncResult turns a create response into either an escaped handle or the final poll response. +func AsyncResult(cmd *cobra.Command, createResponse interface{}, newPoll AsyncPollFactory) error { + started := time.Now() + cfg, err := activeAsync(cmd) + if err != nil { + return renderAsyncError(cmd, protocolAsyncError(nil, "", "", "invalid generated async configuration: "+err.Error(), nil)) + } + timings, err := effectiveAsyncTimings(cmd, cfg) + if err != nil { + return err + } + create, err := snapshotAsyncResponse(createResponse) + if err != nil { + return renderAsyncError(cmd, protocolAsyncError(cfg, "", cfg.Resume, "create response: "+err.Error(), nil)) + } + idValue, ok := asyncPointer(create.parsed, cfg.IDPointer) + id, isString := idValue.(string) + if !ok || !isString || id == "" { + return renderAsyncError(cmd, protocolAsyncError(cfg, "", cfg.Resume, "create response does not contain a non-empty string handle at "+cfg.IDPointer, create.parsed)) + } + resume := asyncResume(cfg.Resume, id) + + escape, _ := flagutil.GetBoolFlag(cmd, "async") + if escape { + if IsMachineMode(cmd) { + return renderAsyncSnapshot(cmd, create, false) + } + fmt.Fprintln(cmd.OutOrStdout(), id) + return nil + } + + poll, err := newPoll(cmd, id, cfg.Params) + if err != nil { + return Error(cmd, err) + } + interval := timings.interval + deadline := started.Add(timings.timeout) + pollCtx, cancel := context.WithDeadline(cmd.Context(), deadline) + defer cancel() + + for { + timer := time.NewTimer(interval) + select { + case <-timer.C: + case <-pollCtx.Done(): + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + if errors.Is(pollCtx.Err(), context.DeadlineExceeded) && cmd.Context().Err() == nil { + return renderAsyncError(cmd, &asyncRuntimeError{reason: ReasonCLIAsyncTimeout, message: fmt.Sprintf("timed out waiting for %s", cfg.Command), id: id, resume: resume}) + } + return Error(cmd, fmt.Errorf("waiting for %s (%s) canceled: %w", cfg.Command, id, pollCtx.Err())) + } + + response, err := poll(pollCtx) + if err != nil { + if errors.Is(pollCtx.Err(), context.DeadlineExceeded) && cmd.Context().Err() == nil { + return renderAsyncError(cmd, &asyncRuntimeError{reason: ReasonCLIAsyncTimeout, message: fmt.Sprintf("timed out waiting for %s", cfg.Command), id: id, resume: resume}) + } + return Error(cmd, err) + } + snapshot, err := snapshotAsyncResponse(response) + if err != nil { + return renderAsyncError(cmd, protocolAsyncError(cfg, id, cfg.Resume, "poll response: "+err.Error(), nil)) + } + stateValue, ok := asyncPointer(snapshot.parsed, cfg.StatePointer) + state, isString := stateValue.(string) + if !ok || !isString || state == "" { + return renderAsyncError(cmd, protocolAsyncError(cfg, id, cfg.Resume, "poll response does not contain a non-empty string state at "+cfg.StatePointer, snapshot.parsed)) + } + classification, known := cfg.States[state] + if !known { + return renderAsyncError(cmd, &asyncRuntimeError{reason: ReasonCLIAsyncUnknownState, message: fmt.Sprintf("poll response returned unknown state %q", state), id: id, resume: resume, payload: snapshot.parsed, errorField: cfg.ErrorField}) + } + + switch classification { + case "success": + return renderAsyncSnapshot(cmd, snapshot, true) + case "handoff": + return renderAsyncSnapshot(cmd, snapshot, false) + case "failure": + message := fmt.Sprintf("%s reached failure state %q", cfg.Command, state) + if detail := asyncFailureDetail(cfg, snapshot.parsed); detail != "" { + message += ": " + detail + } + return renderAsyncError(cmd, &asyncRuntimeError{reason: ReasonCLIAsyncFailed, message: message, id: id, resume: resume, payload: snapshot.parsed, errorField: cfg.ErrorField}) + case "pending": + if !IsMachineMode(cmd) { + fmt.Fprintf(cmd.ErrOrStderr(), "Waiting for %s (%s: %s, %s elapsed)…%s", cfg.Command, id, state, asyncElapsed(time.Since(started)), string(rune(10))) + } + default: + return renderAsyncError(cmd, &asyncRuntimeError{reason: ReasonCLIProtocol, message: fmt.Sprintf("generated async table has invalid classification %q for state %q", classification, state), id: id, resume: resume, payload: snapshot.parsed, errorField: cfg.ErrorField}) + } + + next := time.Duration(float64(interval) * cfg.Backoff) + if next < interval || next > timings.maxInterval { + next = timings.maxInterval + } + interval = next + } +} + +func activeAsync(cmd *cobra.Command) (*asyncConfig, error) { + if cmd == nil || cmd.Annotations == nil { + return nil, errors.New("command has no async annotation") + } + raw := cmd.Annotations[asyncAnnotation] + var cfg asyncConfig + dec := json.NewDecoder(strings.NewReader(raw)) + dec.UseNumber() + if err := dec.Decode(&cfg); err != nil { + return nil, err + } + if cfg.IDPointer == "" || cfg.StatePointer == "" || len(cfg.States) == 0 || cfg.Resume == "" { + return nil, errors.New("async annotation is incomplete") + } + interval, err := time.ParseDuration(cfg.Interval) + if err != nil || interval <= 0 { + return nil, errors.New("async interval is invalid") + } + maximum, err := time.ParseDuration(cfg.MaxInterval) + if err != nil || maximum < interval { + return nil, errors.New("async max interval is invalid") + } + timeout, err := time.ParseDuration(cfg.Timeout) + if err != nil || timeout < interval { + return nil, errors.New("async timeout is invalid") + } + if cfg.Backoff < 1 { + return nil, errors.New("async backoff is invalid") + } + if cfg.ErrorField == "" { + cfg.ErrorField = "error" + } + if cfg.ErrorMessageField == "" { + cfg.ErrorMessageField = "message" + } + return &cfg, nil +} + +func ValidateAsyncFlags(cmd *cobra.Command) error { + cfg, err := activeAsync(cmd) + if err != nil { + return WithCLIReason(fmt.Errorf("invalid generated async configuration: %w", err), ReasonCLIProtocol) + } + _, err = effectiveAsyncTimings(cmd, cfg) + return err +} + +func effectiveAsyncTimings(cmd *cobra.Command, cfg *asyncConfig) (asyncTimings, error) { + interval, _ := time.ParseDuration(cfg.Interval) + maximum, _ := time.ParseDuration(cfg.MaxInterval) + timeout, _ := time.ParseDuration(cfg.Timeout) + + if raw, changed := flagutil.GetStringFlag(cmd, "poll-interval"); changed { + parsed, err := time.ParseDuration(raw) + if err != nil || parsed <= 0 { + return asyncTimings{}, flagutil.WithCLIValidation(fmt.Errorf("invalid --poll-interval %q: must be a positive Go duration", raw)) + } + interval = parsed + } + if maximum < interval { + maximum = interval + } + if raw, changed := flagutil.GetStringFlag(cmd, "poll-timeout"); changed { + parsed, err := time.ParseDuration(raw) + if err != nil || parsed <= 0 { + return asyncTimings{}, flagutil.WithCLIValidation(fmt.Errorf("invalid --poll-timeout %q: must be a positive Go duration", raw)) + } + timeout = parsed + } + if timeout < interval { + return asyncTimings{}, flagutil.WithCLIValidation(fmt.Errorf("--poll-timeout (%s) must be greater than or equal to the effective poll interval (%s)", timeout, interval)) + } + return asyncTimings{interval: interval, maxInterval: maximum, timeout: timeout}, nil +} + +func snapshotAsyncResponse(response interface{}) (asyncSnapshot, error) { + if httpResponse := extractHTTPResponse(response); httpResponse != nil { + contentType := strings.ToLower(httpResponse.Header.Get("Content-Type")) + if strings.Contains(contentType, "text/event-stream") || strings.Contains(contentType, "application/jsonl") || strings.Contains(contentType, "application/json-seq") { + return asyncSnapshot{}, fmt.Errorf("server returned a stream (%s) where a discrete JSON response was required", contentType) + } + } + headers := extractResponseHeaders(response).Clone() + raw, rawErr := tryReadRawBody(response) + if rawErr != nil { + return asyncSnapshot{}, rawErr + } + if len(raw) == 0 { + content := extractResultContent(response) + if content == nil { + return asyncSnapshot{}, errors.New("response body is empty") + } + var err error + raw, err = marshalJSON(content) + if err != nil { + return asyncSnapshot{}, err + } + } + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + var parsed interface{} + if err := decoder.Decode(&parsed); err != nil { + return asyncSnapshot{}, fmt.Errorf("response body is not valid JSON: %w", err) + } + return asyncSnapshot{raw: raw, parsed: parsed, headers: headers}, nil +} + +func renderAsyncSnapshot(cmd *cobra.Command, snapshot asyncSnapshot, allowArtifact bool) error { + response := &http.Response{ + Body: io.NopCloser(bytes.NewReader(snapshot.raw)), + Header: snapshot.headers.Clone(), + } + envelope := &asyncResultEnvelope{ + HTTPMeta: asyncHTTPMeta{Response: response}, + Result: snapshot.parsed, + } + return result(cmd, envelope, allowArtifact) +} + +func asyncPointer(root interface{}, pointer string) (interface{}, bool) { + if pointer == "" { + return root, true + } + if !strings.HasPrefix(pointer, "/") { + return nil, false + } + current := root + for _, raw := range strings.Split(strings.TrimPrefix(pointer, "/"), "/") { + segment := strings.ReplaceAll(strings.ReplaceAll(raw, "~1", "/"), "~0", "~") + switch value := current.(type) { + case map[string]interface{}: + var ok bool + current, ok = value[segment] + if !ok { + return nil, false + } + case []interface{}: + index, err := strconv.Atoi(segment) + if err != nil || index < 0 || index >= len(value) { + return nil, false + } + current = value[index] + default: + return nil, false + } + } + return current, true +} + +func protocolAsyncError(cfg *asyncConfig, id, resumePrefix, message string, payload interface{}) *asyncRuntimeError { + resume := resumePrefix + if id != "" && id != "" { + resume = asyncResume(resumePrefix, id) + } else if resume != "" { + resume += " " + } + errorField := "" + if cfg != nil { + errorField = cfg.ErrorField + } + return &asyncRuntimeError{reason: ReasonCLIProtocol, message: message, id: id, resume: resume, payload: payload, errorField: errorField} +} + +func renderAsyncError(cmd *cobra.Command, runtimeErr *asyncRuntimeError) error { + jqExpr, _ := flagutil.GetStringFlag(cmd, "jq") + if !shouldRenderStructuredError(resolveOutputFormat(cmd), jqExpr) { + fmt.Fprintf(cmd.ErrOrStderr(), "Error: %s (handle %s; resume: %s)%s", runtimeErr.message, runtimeErr.id, runtimeErr.resume, string(rune(10))) + classification := Classify(cmd, withErrorOrigin(runtimeErr, ErrorOriginCLI)) + return markRendered(runtimeErr, classification) + } + return CLIError(cmd, runtimeErr) +} + +func asyncResume(prefix, id string) string { + if id == "" { + return prefix + } + safe := true + for _, char := range id { + if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/", char) { + safe = false + break + } + } + if safe { + return prefix + " " + id + } + return prefix + " '" + strings.ReplaceAll(id, "'", "'\"'\"'") + "'" +} + +func asyncElapsed(elapsed time.Duration) string { + if elapsed < time.Second { + return "0s" + } + return elapsed.Round(time.Second).String() +} + +// asyncFailureDetail extracts the human failure message through the +// declared bindings (default "error"/"message"). +func asyncFailureDetail(cfg *asyncConfig, payload interface{}) string { + object, ok := payload.(map[string]interface{}) + if !ok { + return "" + } + errorMember, ok := object[cfg.ErrorField].(map[string]interface{}) + if !ok { + return "" + } + message, _ := errorMember[cfg.ErrorMessageField].(string) + return message +} diff --git a/internal/output/classify.go b/internal/output/classify.go new file mode 100644 index 0000000..f2346ec --- /dev/null +++ b/internal/output/classify.go @@ -0,0 +1,713 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/url" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/clierrors" + "github.com/spf13/cobra" +) + +type ErrorType string + +const ( + ErrorTypeAuthentication ErrorType = "authentication_error" + ErrorTypeAuthorization ErrorType = "authorization_error" + ErrorTypeServiceDisabled ErrorType = "service_disabled" + ErrorTypeBillingDisabled ErrorType = "billing_disabled" + ErrorTypeNotFound ErrorType = "not_found" + ErrorTypeValidation ErrorType = "validation_error" + ErrorTypeRateLimit ErrorType = "rate_limit_error" + ErrorTypeServer ErrorType = "server_error" + ErrorTypeConnection ErrorType = "connection_error" + ErrorTypeProtocol ErrorType = "protocol_error" + ErrorTypeAPI ErrorType = "api_error" + ErrorTypeRuntime ErrorType = "runtime_error" + ErrorTypeUnsupported ErrorType = "unsupported_error" + ErrorTypeAsyncFailed ErrorType = "async_failed" + ErrorTypeAsyncTimeout ErrorType = "async_timeout" + ErrorTypeAsyncUnknownState ErrorType = "async_unknown_state" +) + +type ErrorOrigin string + +const ( + ErrorOriginAPI ErrorOrigin = "api" + ErrorOriginCLI ErrorOrigin = "cli" + ErrorOriginStream ErrorOrigin = "stream" +) + +type Classification struct { + Type ErrorType + Reason string + StatusCode int + Message string + Hints []string + Body any + RawBody string + Origin ErrorOrigin + + // reasonCarrier is the carrier pointer whose value was selected as + // Reason, recorded so pretty details strip only the member actually + // surfaced on the Reason: line. It is nil when Reason did not come from + // a body carrier (runtime CLI reasons, synthetic reasons, no reason). + reasonCarrier []reasonCarrierSegment + // reasonCarrierPath pins the concrete occurrence within reasonCarrier + // that supplied Reason: the array index chosen at each [*] hop, in + // segment order. Several array entries can share the selected code, and + // only the recorded occurrence was surfaced. Empty when the carrier has + // no wildcard hops. + reasonCarrierPath []int +} + +type errorRule struct { + Type ErrorType + Hints []string +} + +var builtinTypeHints = map[ErrorType][]string{ + ErrorTypeAuthentication: { + "Set GEMINI_API_KEY (or another credential via the GEMINI_* environment variables / the credential flags listed in --help)", + fmt.Sprintf("Run '%s whoami' to check current authentication status", cliName), + }, + ErrorTypeAuthorization: { + "The caller is not permitted to access this resource", + fmt.Sprintf("Run '%s whoami' to check which credentials are in use", cliName), + }, + ErrorTypeServiceDisabled: { + "The API reports this service as disabled for the calling account — enable it before retrying (this is not a credential problem)", + }, + ErrorTypeBillingDisabled: { + "The API reports billing as disabled for the calling account — enable billing before retrying (this is not a credential problem)", + }, + ErrorTypeNotFound: { + "Verify the resource identifier is correct", + }, + ErrorTypeValidation: { + "Use --dry-run to preview the request and check parameters", + "Run the command with --help for runnable examples, or --usage for the machine-readable surface", + }, + ErrorTypeRateLimit: { + "Rate limited — retry after a delay", + }, + ErrorTypeServer: { + "The server failed to process the request — retry after a short delay", + }, + ErrorTypeConnection: { + "Check network connectivity", + "Verify the server URL with --server-url", + }, + ErrorTypeProtocol: { + "The server responded, but the response could not be decoded against the schema this CLI was built from", + fmt.Sprintf("Retrying will not help; report the decode error together with the output of '%s version'", cliName), + }, + ErrorTypeAPI: { + "Review the server message and retry with --debug if more response detail is needed", + }, + ErrorTypeRuntime: { + "Re-run with --debug for the full failure detail", + }, + ErrorTypeUnsupported: { + "This command is not available in this build; run --usage to list the supported surface", + }, + ErrorTypeAsyncFailed: { + "The background operation finished with a failure; the message carries the terminal state", + }, + ErrorTypeAsyncTimeout: { + "The background operation did not reach a terminal state in time; use --async to get a handle and poll it later", + }, + ErrorTypeAsyncUnknownState: { + "The background operation reported a state this CLI does not understand; inspect it with the raw operation command", + }, +} + +func ClassificationFrom(err error) (Classification, bool) { + var classified interface{ Classification() Classification } + if !errors.As(err, &classified) { + return Classification{}, false + } + return classified.Classification(), true +} + +type errorOriginMarker struct { + error + origin ErrorOrigin +} + +func (e errorOriginMarker) ErrorOrigin() ErrorOrigin { return e.origin } +func (e errorOriginMarker) Unwrap() error { return e.error } + +func withErrorOrigin(err error, origin ErrorOrigin) error { + if err == nil { + return nil + } + return errorOriginMarker{error: err, origin: origin} +} + +func Classify(cmd *cobra.Command, err error) Classification { + if err == nil { + return Classification{} + } + if classification, ok := ClassificationFrom(err); ok { + return classification + } + + c := Classification{ + StatusCode: extractErrorStatusCode(err), + RawBody: extractErrorBody(err), + Origin: ErrorOriginAPI, + } + var origin interface{ ErrorOrigin() ErrorOrigin } + if errors.As(err, &origin) { + c.Origin = origin.ErrorOrigin() + } + var streamErr *StreamEventError + if errors.As(err, &streamErr) { + c.Origin = ErrorOriginStream + } + + bodyMap, errorObject := parseClassificationBody(c.RawBody, &c) + useReasonRule := false + var ruleHints []string + + var explicitReason interface{ CLIReason() string } + var coded clierrors.ExitCoder + if errors.As(err, &explicitReason) { + c.Reason = normalizeCLIReason(explicitReason.CLIReason()) + c.Type = cliReasonErrorType(c.Reason) + c.Origin = ErrorOriginCLI + useReasonRule = true + ruleHints = runtimeReasonHints[c.Reason] + } else if c.StatusCode == 0 && errors.As(err, &coded) && exitCodeReason(clierrors.ExitCode(err)) != "" { + c.Reason = exitCodeReason(clierrors.ExitCode(err)) + c.Type = cliReasonErrorType(c.Reason) + c.Origin = ErrorOriginCLI + useReasonRule = true + ruleHints = runtimeReasonHints[c.Reason] + } else { + probeOccurrences := collectProbeOccurrences(errorObject) + // Reason selection: the first carrier that yields a reason owns + // error_reason — carriers in declaration order. Within a carrier, a + // typed rule wins over a hints-only rule, which wins over verbatim + // promotion (available only when the carrier's pointer is explicitly + // declared). + for i := range errorProbes { + if probeSelectsReason(&c, errorProbes[i], probeOccurrences[i], &ruleHints) { + c.reasonCarrier = errorProbes[i].segments + useReasonRule = true + break + } + } + // Type fallback: when the selected reason carries no declared type, + // a typed rule matched at any declared carrier still supplies the + // error type (its hints stay with the reason-selecting rule). + if c.Type == "" { + typeScan: + for i, probe := range errorProbes { + for _, occurrence := range probeOccurrences[i] { + if rule, ok := probe.rules[occurrence.value]; ok && rule.Type != "" { + c.Type = rule.Type + break typeScan + } + } + } + } + if c.Type == "" && c.StatusCode != 0 { + c.Type = classifyHTTPStatus(c.StatusCode) + } + if c.Type == "" { + c.Type = classifyStatuslessError(err, c.Origin) + } + if c.Reason == "" && c.StatusCode == 0 { + c.Reason = syntheticCLIReason(c.Type) + useReasonRule = c.Reason != "" + ruleHints = runtimeReasonHints[c.Reason] + } + } + if c.StatusCode == 0 && c.Origin == ErrorOriginAPI { + switch c.Type { + case ErrorTypeValidation, ErrorTypeRuntime, ErrorTypeUnsupported: + c.Origin = ErrorOriginCLI + } + } + + c.Message = classificationMessage(bodyMap, errorObject, err, c.RawBody) + c.Hints = assembleHints(cmd, err, c, bodyMap, errorObject, ruleHints, useReasonRule) + return c +} + +func parseClassificationBody(raw string, c *Classification) (map[string]any, map[string]any) { + if raw == "" { + return nil, map[string]any{} + } + if !json.Valid([]byte(raw)) { + return nil, map[string]any{} + } + decoder := json.NewDecoder(bytes.NewBufferString(raw)) + decoder.UseNumber() + var parsed any + if err := decoder.Decode(&parsed); err != nil { + return nil, map[string]any{} + } + if unwrapped := unwrapArrayWrappedError(parsed); unwrapped != nil { + parsed = unwrapped + } + c.Body = parsed + bodyMap, _ := parsed.(map[string]any) + if bodyMap == nil { + return nil, map[string]any{} + } + if nested, ok := bodyMap["error"].(map[string]any); ok { + return bodyMap, nested + } + if nestedArray, ok := bodyMap["error"].([]any); ok && len(nestedArray) == 1 { + if nested, ok := nestedArray[0].(map[string]any); ok { + return bodyMap, nested + } + } + return bodyMap, bodyMap +} + +// unwrapArrayWrappedError normalizes the single-element array-wrapped error +// body shape ([{"error":{…}}]) this document opted into via the +// x-speakeasy-cli-errors unwrapErrorArray declaration: the sole element +// replaces the array before classification. Any other array shape is left +// untouched. +func unwrapArrayWrappedError(value any) map[string]any { + items, ok := value.([]any) + if !ok || len(items) != 1 { + return nil + } + first, ok := items[0].(map[string]any) + if !ok { + return nil + } + if _, ok := first["error"].(map[string]any); !ok { + return nil + } + return first +} + +// reasonCarrierSegment is one step of a reason-carrier pointer: a named +// member, or a [*] fan-out over every item of an array member. +type reasonCarrierSegment struct { + field string + isWild bool +} + +// errorProbe is one reason carrier from x-speakeasy-cli-errors: a pointer +// resolved against the response body's error object, the reason rules +// matched against the string values found there, and whether the carrier was +// explicitly declared — a declared carrier promotes an unmatched reason code +// verbatim as error_reason. +type errorProbe struct { + segments []reasonCarrierSegment + rules map[string]errorRule + promote bool +} + +// reasonOccurrence is one string value found at a carrier, pinned to the +// concrete node that holds it: path records the array index chosen at each +// [*] hop, in segment order (empty for a wildcard-free carrier). +type reasonOccurrence struct { + value string + path []int +} + +// occurrences collects the string values found at the probe's carrier, in +// document order, each pinned to the wildcard indices that reached it. +func (p errorProbe) occurrences(errorObject map[string]any) []reasonOccurrence { + type node struct { + value any + path []int + } + nodes := []node{node{value: errorObject}} + for _, segment := range p.segments { + next := make([]node, 0, len(nodes)) + for _, current := range nodes { + if segment.isWild { + items, _ := current.value.([]any) + for i, item := range items { + path := make([]int, len(current.path), len(current.path)+1) + copy(path, current.path) + next = append(next, node{value: item, path: append(path, i)}) + } + continue + } + object, _ := current.value.(map[string]any) + if child, ok := object[segment.field]; ok { + next = append(next, node{value: child, path: current.path}) + } + } + nodes = next + } + occurrences := make([]reasonOccurrence, 0, len(nodes)) + for _, current := range nodes { + if reason, ok := current.value.(string); ok && reason != "" { + occurrences = append(occurrences, reasonOccurrence{value: reason, path: current.path}) + } + } + return occurrences +} + +func collectProbeOccurrences(errorObject map[string]any) [][]reasonOccurrence { + found := make([][]reasonOccurrence, len(errorProbes)) + for i, probe := range errorProbes { + found[i] = probe.occurrences(errorObject) + } + return found +} + +// probeSelectsReason applies one carrier's rules to the reason values found +// at it. A typed rule wins (setting reason, type, and the rule's hints), then +// a hints-only rule (reason and hints; the type falls through to later +// evidence), then — for an explicitly declared carrier — the first value is +// promoted verbatim as the reason. Each tier selects the first occurrence in +// document order, and the winning occurrence's wildcard path is recorded so +// pretty details strip exactly the member that supplied the reason. Returns +// false when the carrier yields no reason so the next declared carrier is +// consulted. +func probeSelectsReason(c *Classification, probe errorProbe, occurrences []reasonOccurrence, ruleHints *[]string) bool { + var hintsOnly reasonOccurrence + var hintsOnlyHints []string + for _, occurrence := range occurrences { + rule, ok := probe.rules[occurrence.value] + if !ok { + continue + } + if rule.Type != "" { + c.Type = rule.Type + c.Reason = occurrence.value + c.reasonCarrierPath = occurrence.path + *ruleHints = rule.Hints + return true + } + if hintsOnly.value == "" && rule.Hints != nil { + hintsOnly = occurrence + hintsOnlyHints = rule.Hints + } + } + if hintsOnly.value != "" { + c.Reason = hintsOnly.value + c.reasonCarrierPath = hintsOnly.path + *ruleHints = hintsOnlyHints + return true + } + if probe.promote && len(occurrences) > 0 { + c.Reason = occurrences[0].value + c.reasonCarrierPath = occurrences[0].path + *ruleHints = nil + return true + } + return false +} + +func classifyHTTPStatus(statusCode int) ErrorType { + switch { + case statusCode == 401: + return ErrorTypeAuthentication + case statusCode == 403: + return ErrorTypeAuthorization + case statusCode == 404: + return ErrorTypeNotFound + case statusCode == 400 || statusCode == 422: + return ErrorTypeValidation + case statusCode == 429: + return ErrorTypeRateLimit + case statusCode >= 500: + return ErrorTypeServer + default: + return ErrorTypeAPI + } +} + +func classifyStatuslessError(err error, origin ErrorOrigin) ErrorType { + var streamErr *StreamEventError + if errors.As(err, &streamErr) { + return ErrorTypeAPI + } + if origin == ErrorOriginCLI { + if isConnectionError(err) { + return ErrorTypeConnection + } + return ErrorTypeRuntime + } + if isProtocolError(err) { + return ErrorTypeProtocol + } + if isConnectionError(err) { + return ErrorTypeConnection + } + return ErrorTypeRuntime +} + +func exitCodeReason(code int) string { + switch code { + case clierrors.ExitAuth: + return ReasonCLIAuthentication + case clierrors.ExitUsage: + return ReasonCLIValidation + } + return "" +} + +func classificationMessage(bodyMap, errorObject map[string]any, err error, rawBody string) string { + if message, _ := errorObject["message"].(string); message != "" { + return message + } + if message, _ := bodyMap["message"].(string); message != "" { + return message + } + if message, _ := bodyMap["error"].(string); message != "" { + return message + } + message := err.Error() + if rawBody == "" { + return message + } + if strings.HasSuffix(message, "\n"+rawBody) { + if trimmed := strings.TrimSuffix(message, "\n"+rawBody); trimmed != "" { + return trimmed + } + } + if strings.HasSuffix(message, rawBody) { + if trimmed := strings.TrimSuffix(message, rawBody); trimmed != "" { + return trimmed + } + } + return message +} + +func assembleHints(cmd *cobra.Command, err error, c Classification, bodyMap, errorObject map[string]any, ruleHints []string, useReasonRule bool) []string { + hints := serverHints(bodyMap, errorObject) + if useReasonRule && ruleHints != nil { + hints = append(hints, ruleHints...) + } else if declared, ok := declaredTypeHints[c.Type]; ok { + hints = append(hints, declared...) + } else { + hints = append(hints, builtinTypeHints[c.Type]...) + } + hints = append(hints, errorCLIHints(err)...) + if declared := declaredCommandHints(cmd); declared != nil && c.Reason != "" { + hints = append(hints, declared[c.Reason]...) + } + seen := make(map[string]bool, len(hints)) + deduped := make([]string, 0, len(hints)) + for _, hint := range hints { + if hint == "" || seen[hint] { + continue + } + seen[hint] = true + deduped = append(deduped, hint) + } + return deduped +} + +func serverHints(bodyMap, errorObject map[string]any) []string { + for _, source := range []map[string]any{errorObject, bodyMap} { + raw, exists := source["hints"] + if !exists { + continue + } + items, ok := raw.([]any) + if !ok { + continue + } + var hints []string + for _, item := range items { + if hint, ok := item.(string); ok && hint != "" { + hints = append(hints, hint) + } + } + if len(hints) > 0 { + return hints + } + } + return nil +} + +const commandHintsAnnotation = "speakeasy_command_hints" + +func declaredCommandHints(cmd *cobra.Command) map[string][]string { + if cmd == nil { + return nil + } + raw, ok := cmd.Annotations[commandHintsAnnotation] + if !ok || raw == "" { + return nil + } + var hints map[string][]string + if err := json.Unmarshal([]byte(raw), &hints); err != nil { + return nil + } + return hints +} + +func errorCLIHints(err error) []string { + var typed interface{ CLIHints() []string } + if errors.As(err, &typed) { + return typed.CLIHints() + } + return nil +} + +func syntheticCLIReason(errorType ErrorType) string { + switch errorType { + case ErrorTypeValidation: + return ReasonCLIValidation + case ErrorTypeAuthentication: + return ReasonCLIAuthentication + case ErrorTypeConnection: + return ReasonCLIConnection + case ErrorTypeProtocol: + return ReasonCLIProtocol + case ErrorTypeRuntime: + return ReasonCLIRuntime + case ErrorTypeUnsupported: + return ReasonCLIUnavailable + case ErrorTypeAsyncFailed: + return ReasonCLIAsyncFailed + case ErrorTypeAsyncTimeout: + return ReasonCLIAsyncTimeout + case ErrorTypeAsyncUnknownState: + return ReasonCLIAsyncUnknownState + } + return "" +} + +// Must match the generated SDK's wrapper around http.Client.Do failures. +const sdkSendErrorMarker = "error sending request:" + +func isConnectionError(err error) bool { + if err == nil { + return false + } + var urlErr *url.Error + if errors.As(err, &urlErr) { + return urlErr.Op != "parse" + } + var opErr *net.OpError + if errors.As(err, &opErr) { + return true + } + var dnsErr *net.DNSError + if errors.As(err, &dnsErr) { + return true + } + if errors.Is(err, context.DeadlineExceeded) { + return true + } + return strings.HasPrefix(err.Error(), sdkSendErrorMarker) +} + +const sdkDecodeErrorMarker = "error unmarshaling json response body:" + +func isProtocolError(err error) bool { + if err == nil { + return false + } + var syntaxErr *json.SyntaxError + var typeErr *json.UnmarshalTypeError + if errors.As(err, &syntaxErr) || errors.As(err, &typeErr) { + return true + } + return strings.Contains(err.Error(), sdkDecodeErrorMarker) +} + +// The generator validates x-speakeasy-cli-commands hint reasons against this list. +const ( + ReasonCLIValidation = "CLI_VALIDATION" + ReasonCLIConnection = "CLI_CONNECTION" + ReasonCLIProtocol = "CLI_PROTOCOL" + ReasonCLIRuntime = "CLI_RUNTIME" + ReasonCLIUnavailable = "CLI_UNAVAILABLE" + ReasonCLIAuthentication = "CLI_AUTHENTICATION" + ReasonCLIAsyncFailed = "CLI_ASYNC_FAILED" + ReasonCLIAsyncTimeout = "CLI_ASYNC_TIMEOUT" + ReasonCLIAsyncUnknownState = "CLI_ASYNC_UNKNOWN_STATE" +) + +type cliReasonError struct { + error + reason string +} + +func (e cliReasonError) CLIReason() string { return e.reason } +func (e cliReasonError) Unwrap() error { return e.error } + +func WithCLIReason(err error, reason string) error { + if err == nil { + return nil + } + return cliReasonError{error: err, reason: normalizeCLIReason(reason)} +} + +func normalizeCLIReason(reason string) string { + switch reason { + case ReasonCLIValidation, ReasonCLIConnection, ReasonCLIProtocol, + ReasonCLIRuntime, ReasonCLIUnavailable, ReasonCLIAuthentication, + ReasonCLIAsyncFailed, ReasonCLIAsyncTimeout, ReasonCLIAsyncUnknownState: + return reason + } + return ReasonCLIRuntime +} + +func cliReasonErrorType(reason string) ErrorType { + switch reason { + case ReasonCLIAuthentication: + return ErrorTypeAuthentication + case ReasonCLIValidation: + return ErrorTypeValidation + case ReasonCLIConnection: + return ErrorTypeConnection + case ReasonCLIProtocol: + return ErrorTypeProtocol + case ReasonCLIUnavailable: + return ErrorTypeUnsupported + case ReasonCLIAsyncFailed: + return ErrorTypeAsyncFailed + case ReasonCLIAsyncTimeout: + return ErrorTypeAsyncTimeout + case ReasonCLIAsyncUnknownState: + return ErrorTypeAsyncUnknownState + default: + return ErrorTypeRuntime + } +} + +type cliHintsError struct { + error + hints []string +} + +func (e cliHintsError) CLIHints() []string { return e.hints } +func (e cliHintsError) Unwrap() error { return e.error } + +func withCLIHints(err error, hints []string) error { + if err == nil || len(hints) == 0 { + return err + } + return cliHintsError{error: err, hints: hints} +} diff --git a/internal/output/color.go b/internal/output/color.go new file mode 100644 index 0000000..f5ca83f --- /dev/null +++ b/internal/output/color.go @@ -0,0 +1,141 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "bytes" + "os" + + "golang.org/x/term" +) + +// ANSI color codes for JSON syntax highlighting. +const ( + colorReset = "\033[0m" + colorKey = "\033[1;34m" // Bold blue + colorString = "\033[32m" // Green + colorNumber = "\033[33m" // Yellow + colorBool = "\033[35m" // Magenta + colorNull = "\033[36m" // Cyan +) + +// ShouldColorize determines whether output should include ANSI color codes. +// It checks agent mode, the --color flag value, NO_COLOR / FORCE_COLOR env vars, and TTY status. +func ShouldColorize(colorFlag string) bool { + // Agent mode: never colorize — output must be machine-parseable. + if IsAgentMode() { + return false + } + switch colorFlag { + case "never": + return false + case "always": + return true + default: // "auto" + // https://no-color.org/ + if _, ok := os.LookupEnv("NO_COLOR"); ok { + return false + } + if _, ok := os.LookupEnv("FORCE_COLOR"); ok { + return true + } + return term.IsTerminal(int(os.Stdout.Fd())) + } +} + +// ColorizeJSON adds ANSI color codes to formatted JSON output. +// It distinguishes keys from string values, and highlights numbers, booleans, and null. +func ColorizeJSON(data []byte) []byte { + var buf bytes.Buffer + buf.Grow(len(data) + len(data)/4) // pre-allocate ~125% for color codes + + i := 0 + n := len(data) + + for i < n { + switch { + case data[i] == '"': + // Read the full string (handles escape sequences) + strStart := i + i++ // skip opening quote + for i < n { + if data[i] == '\\' { + if i+1 < n { + i += 2 // skip escape sequence + } else { + // Trailing backslash in malformed JSON; consume safely. + i++ + } + continue + } + if data[i] == '"' { + i++ // skip closing quote + break + } + i++ + } + strBytes := data[strStart:i] + + // Peek ahead: if ':' follows (ignoring whitespace), this is a key + j := i + for j < n && (data[j] == ' ' || data[j] == '\t' || data[j] == '\n' || data[j] == '\r') { + j++ + } + if j < n && data[j] == ':' { + buf.WriteString(colorKey) + } else { + buf.WriteString(colorString) + } + buf.Write(strBytes) + buf.WriteString(colorReset) + + case data[i] == 't' && i+4 <= n && string(data[i:i+4]) == "true": + buf.WriteString(colorBool) + buf.WriteString("true") + buf.WriteString(colorReset) + i += 4 + + case data[i] == 'f' && i+5 <= n && string(data[i:i+5]) == "false": + buf.WriteString(colorBool) + buf.WriteString("false") + buf.WriteString(colorReset) + i += 5 + + case data[i] == 'n' && i+4 <= n && string(data[i:i+4]) == "null": + buf.WriteString(colorNull) + buf.WriteString("null") + buf.WriteString(colorReset) + i += 4 + + case data[i] == '-' || (data[i] >= '0' && data[i] <= '9'): + start := i + i++ + for i < n && (data[i] >= '0' && data[i] <= '9' || data[i] == '.' || data[i] == 'e' || data[i] == 'E' || data[i] == '+' || data[i] == '-') { + i++ + } + buf.WriteString(colorNumber) + buf.Write(data[start:i]) + buf.WriteString(colorReset) + + default: + buf.WriteByte(data[i]) + i++ + } + } + + return buf.Bytes() +} diff --git a/internal/output/errortable.go b/internal/output/errortable.go new file mode 100644 index 0000000..5174c3e --- /dev/null +++ b/internal/output/errortable.go @@ -0,0 +1,174 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +// Generated from the x-speakeasy-cli-errors document extension. + +// errorProbes lists this build's reason carriers in classification order: +// the primary carrier (the declared reasonPointer, default $.reason) first, +// then each declared probe. +var errorProbes = []errorProbe{ + { + // $.details[*].reason + segments: []reasonCarrierSegment{ + {field: "details"}, + {isWild: true}, + {field: "reason"}, + }, + rules: map[string]errorRule{ + "ACCESS_TOKEN_EXPIRED": { + Type: ErrorType("authentication_error"), + Hints: []string{ + "The OAuth access token has expired. Refresh it and pass the new value with --access-token (or use an API key via GEMINI_API_KEY)", + }, + }, + "ACCESS_TOKEN_SCOPE_INSUFFICIENT": { + Type: ErrorType("authorization_error"), + Hints: []string{ + "The access token lacks the scope this call needs (https://www.googleapis.com/auth/generative-language). Re-authorize with that scope, or use an API key via GEMINI_API_KEY", + }, + }, + "ACCESS_TOKEN_TYPE_UNSUPPORTED": { + Type: ErrorType("authentication_error"), + Hints: []string{ + "This token type is not accepted here. Use an API key (GEMINI_API_KEY) or a bearer OAuth access token via --access-token", + }, + }, + "API_KEY_HTTP_REFERRER_BLOCKED": { + Type: ErrorType("authorization_error"), + Hints: []string{ + "This API key only accepts requests from allow-listed HTTP referrers, which a CLI does not send. Use a key without referrer restrictions", + }, + }, + "API_KEY_INVALID": { + Type: ErrorType("authentication_error"), + Hints: []string{ + "The API key was rejected. Create a key at https://aistudio.google.com/apikey, then export GEMINI_API_KEY= (or run \"gemini-api configure\" to store it)", + "Run \"gemini-api whoami\" to see which credential is being sent", + }, + }, + "API_KEY_IP_ADDRESS_BLOCKED": { + Type: ErrorType("authorization_error"), + Hints: []string{ + "This API key only accepts requests from allow-listed IP addresses. Add this machine's address to the key's restrictions or use a key without IP restrictions", + }, + }, + "API_KEY_MISSING": { + Type: ErrorType("authentication_error"), + Hints: []string{ + "No API key was sent. Export GEMINI_API_KEY= (create one at https://aistudio.google.com/apikey) or run \"gemini-api configure\"", + }, + }, + "API_KEY_SERVICE_BLOCKED": { + Type: ErrorType("authorization_error"), + Hints: []string{ + "This API key is restricted from calling the Gemini API. Edit the key's API restrictions in Google AI Studio / Cloud Console, or use an unrestricted key", + }, + }, + "BILLING_DISABLED": { + Type: ErrorType("billing_disabled"), + Hints: []string{ + "Billing is disabled for the project that owns this key (this is not a credential problem). Enable billing on the project or use a key from a project with billing enabled; the free tier may not be available in your region", + }, + }, + "CONSUMER_SUSPENDED": { + Type: ErrorType("authorization_error"), + Hints: []string{ + "The Google Cloud project behind this key is suspended (this is not a credential problem). Check the project status in the Cloud Console", + }, + }, + "CREDENTIALS_MISSING": { + Type: ErrorType("authentication_error"), + Hints: []string{ + "No credential was sent. Export GEMINI_API_KEY= (create one at https://aistudio.google.com/apikey) or run \"gemini-api configure\"", + }, + }, + "QUOTA_EXCEEDED": { + Type: ErrorType("rate_limit_error"), + Hints: []string{ + "Quota exhausted for this model or project. Wait for the quota window to reset, request a higher tier at https://ai.google.dev/gemini-api/docs/rate-limits, or pick another model with --model", + }, + }, + "RATE_LIMIT_EXCEEDED": { + Type: ErrorType("rate_limit_error"), + Hints: []string{ + "Rate limit hit for this model. Back off and retry, or pick a lighter model with --model; limits per model are at https://ai.google.dev/gemini-api/docs/rate-limits", + }, + }, + "RESOURCE_EXHAUSTED": { + Type: ErrorType("rate_limit_error"), + Hints: []string{ + "Quota exhausted. Wait and retry with backoff, or pick another model with --model; limits are at https://ai.google.dev/gemini-api/docs/rate-limits", + }, + }, + "SERVICE_DISABLED": { + Type: ErrorType("service_disabled"), + Hints: []string{ + "The Generative Language API is disabled for the project that owns this key (this is not a credential problem). Enable it at https://console.developers.google.com/apis/api/generativelanguage.googleapis.com/overview?project=, then retry after a few minutes", + }, + }, + "USER_PROJECT_DENIED": { + Type: ErrorType("authorization_error"), + Hints: []string{ + "The caller may not bill the quota project named by --user-project / GEMINI_USER_PROJECT. Use a project you have serviceusage.services.use on, or drop the flag", + }, + }, + }, + promote: true, + }, + { + // $.status + segments: []reasonCarrierSegment{ + {field: "status"}, + }, + rules: map[string]errorRule{ + "NOT_FOUND": { + Type: ErrorType("not_found"), + }, + "PERMISSION_DENIED": { + Type: ErrorType("authorization_error"), + }, + "RESOURCE_EXHAUSTED": { + Type: ErrorType("rate_limit_error"), + }, + "UNAUTHENTICATED": { + Type: ErrorType("authentication_error"), + }, + }, + promote: true, + }, +} + +// runtimeReasonHints replaces the generated guidance for the runtime-owned +// CLI_* reasons the local runtime emits. +var runtimeReasonHints = map[string][]string{} + +var declaredTypeHints = map[ErrorType][]string{ + ErrorType("authentication_error"): { + "Set GEMINI_API_KEY to a valid key from https://aistudio.google.com/apikey, or run \"gemini-api configure\" to store one", + "Run \"gemini-api whoami\" to see which credential is being sent", + }, + ErrorType("billing_disabled"): { + "Billing is disabled for the calling account or project — enable billing (this is not a credential problem)", + }, + ErrorType("not_found"): { + "Check the resource name (models are \"gemini-…\"; files are \"files/\"; agents and interactions have distinct ids). \"gemini-api models\" lists the curated models", + }, + ErrorType("service_disabled"): { + "The API service is disabled for the calling account or project — enable it before retrying (this is not a credential problem)", + }, +} diff --git a/internal/output/exitcodes.go b/internal/output/exitcodes.go new file mode 100644 index 0000000..2710d4b --- /dev/null +++ b/internal/output/exitcodes.go @@ -0,0 +1,177 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "fmt" + "io" + "os" + "reflect" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/usage" + "github.com/spf13/cobra" +) + +// InstallErrorHandling runs after usage.Intercept, which rewrites the hooks and Args wrapped here. +func InstallErrorHandling(root *cobra.Command) { + installCommandErrorHandling(root) +} + +func installCommandErrorHandling(cmd *cobra.Command) { + // Descend first: cmd.FlagErrorFunc() falls back to the parent's handler. + for _, child := range cmd.Commands() { + installCommandErrorHandling(child) + } + + flagError := cmd.FlagErrorFunc() + cmd.SetFlagErrorFunc(func(current *cobra.Command, err error) error { + return flagutil.WithCLIValidation(flagError(current, err)) + }) + + if usage.GroupMadeRunnable(cmd) && cmd.Args == nil { + cmd.Args = cobra.NoArgs + } + if argsValidator := cmd.Args; argsValidator != nil { + cmd.Args = func(current *cobra.Command, args []string) error { + return flagutil.WithCLIValidation(argsValidator(current, args)) + } + } + + preRun := cmd.PreRun + preRunE := cmd.PreRunE + cmd.PreRunE = func(current *cobra.Command, args []string) error { + if preRunE != nil { + if err := preRunE(current, args); err != nil { + return err + } + } else if preRun != nil { + // Cobra prefers PreRunE over PreRun when both are set. + preRun(current, args) + } + // A --usage invocation answers with the command's schema regardless + // of the invocation's completeness; requiredness is reported inside + // the schema, so validation must not block discovery. The disarm + // covers cobra's own post-hook validation and is re-armed by the + // RunE wrapper below so a reused command tree keeps enforcing. + if usage.UsageRequested(current) { + usage.DisarmRequiredFlags(current) + return nil + } + // Cobra runs these checks after PreRunE and returns plain errors. + if err := current.ValidateRequiredFlags(); err != nil { + return flagutil.WithCLIValidation(err) + } + if err := current.ValidateFlagGroups(); err != nil { + return flagutil.WithCLIValidation(err) + } + return nil + } + + if runE := cmd.RunE; runE != nil { + cmd.RunE = func(current *cobra.Command, args []string) error { + defer usage.RearmRequiredFlags(current) + return runE(current, args) + } + } +} + +func renderingMode(cmd *cobra.Command) (format, jqExpr string) { + jqExpr, changed := flagutil.GetStringFlag(cmd, "jq") + if !changed || jqExpr == "" { + jqExpr = preparsedRendering.jq + } + return resolveOutputFormat(cmd), jqExpr +} + +func structuredErrorsRequested(cmd *cobra.Command) bool { + format, jqExpr := renderingMode(cmd) + return shouldRenderStructuredError(format, jqExpr) +} + +type usageHelpError struct { + error + helpCommand string +} + +func (usageHelpError) CLIReason() string { return ReasonCLIValidation } + +func (e usageHelpError) CLIHints() []string { + return []string{fmt.Sprintf("Run '%s --help' for usage and examples", e.helpCommand)} +} + +func (e usageHelpError) Unwrap() error { return e.error } + +// sameWriter reports whether two writers are known to be the same value. +// Interface equality panics when both operands hold the same non-comparable +// dynamic type (e.g. a func adapter or a struct value with slice fields), and +// a child command inheriting the root's writer puts the identical value on +// both sides, so == only runs for comparable types. A non-comparable writer +// is conservatively reported as distinct; the caller then pins the resolved +// writer instead of restoring inheritance, which keeps output correct. +func sameWriter(a, b io.Writer) bool { + ta := reflect.TypeOf(a) + if ta != reflect.TypeOf(b) { + return false + } + if ta != nil && !ta.Comparable() { + return false + } + return a == b +} + +// localOutWriter returns the command's own output writer; nil means the +// command resolves its writer through the parent chain, so a later +// SetOut(nil) keeps that inheritance intact. +func localOutWriter(cmd *cobra.Command) io.Writer { + resolved := cmd.OutOrStdout() + if parent := cmd.Parent(); parent != nil { + if sameWriter(resolved, parent.OutOrStdout()) { + return nil + } + } else if resolved == os.Stdout { + return nil + } + return resolved +} + +func UsageHelpError(cmd *cobra.Command, err error) error { + if err == nil { + return nil + } + typed := withErrorOrigin( + usageHelpError{error: err, helpCommand: cmd.CommandPath()}, + ErrorOriginCLI, + ) + classification := Classify(cmd, typed) + if structuredErrorsRequested(cmd) { + return retainClassification(typed, classification) + } + + stderr := cmd.ErrOrStderr() + previousOut := localOutWriter(cmd) + cmd.SetOut(stderr) + helpErr := cmd.Help() + cmd.SetOut(previousOut) + if helpErr != nil { + helpFailure := withErrorOrigin(fmt.Errorf("render help: %w", helpErr), ErrorOriginCLI) + return retainClassification(helpFailure, Classify(cmd, helpFailure)) + } + fmt.Fprintln(stderr) + fmt.Fprintln(stderr, "Error:", err) + return markRendered(typed, classification) +} diff --git a/internal/output/jq.go b/internal/output/jq.go new file mode 100644 index 0000000..9be23ed --- /dev/null +++ b/internal/output/jq.go @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "encoding/json" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/itchyny/gojq" +) + +// ApplyJqFilter applies a jq expression to the given content and returns the results. +// The content is first marshaled to JSON and back to ensure a clean interface{} structure, +// then the jq expression is evaluated against it. +func ApplyJqFilter(content interface{}, expression string) ([]interface{}, error) { + // Marshal to JSON and back to get a clean JSON-compatible structure. + // SDK types have custom marshal methods so we need to go through JSON + // to get plain maps/slices/primitives that gojq can process. + jsonData, err := json.Marshal(content) + if err != nil { + return nil, fmt.Errorf("failed to marshal for jq: %w", err) + } + + var data interface{} + if err := json.Unmarshal(jsonData, &data); err != nil { + return nil, fmt.Errorf("failed to prepare data for jq: %w", err) + } + + query, err := gojq.Parse(expression) + if err != nil { + return nil, flagutil.WithCLIValidation(fmt.Errorf("invalid jq expression %q: %w", expression, err)) + } + + var results []interface{} + iter := query.Run(data) + for { + v, ok := iter.Next() + if !ok { + break + } + if err, isErr := v.(error); isErr { + return nil, fmt.Errorf("jq error: %w", err) + } + results = append(results, v) + } + + return results, nil +} diff --git a/internal/output/output.go b/internal/output/output.go new file mode 100644 index 0000000..54fca07 --- /dev/null +++ b/internal/output/output.go @@ -0,0 +1,1479 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package output provides utilities for formatting and outputting CLI command results. +package output + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "reflect" + "sort" + "strconv" + "strings" + "text/tabwriter" + + "github.com/google-gemini/gemini-api-cli/internal/clierrors" + "github.com/google-gemini/gemini-api-cli/internal/config" + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/spf13/cobra" + "github.com/alpkeskin/gotoon" + "gopkg.in/yaml.v3" +) + +// cliName is the CLI binary name, injected at generation time. +const cliName = "gemini-api" + +type renderedError struct { + error + classification Classification +} + +type classifiedError struct { + error + classification Classification +} + +func (renderedError) Rendered() bool { return true } + +func (r renderedError) Unwrap() error { return r.error } + +func (r renderedError) Classification() Classification { return r.classification } + +func (r classifiedError) Unwrap() error { return r.error } + +func (r classifiedError) Classification() Classification { return r.classification } + +func retainClassification(err error, classification Classification) error { + if err == nil { + return nil + } + if _, ok := ClassificationFrom(err); ok { + return err + } + return classifiedError{error: withClassificationExitCode(err, classification), classification: classification} +} + +func markRendered(err error, classification Classification) error { + if err == nil || IsRendered(err) { + return err + } + return renderedError{error: withClassificationExitCode(err, classification), classification: classification} +} + +func ExitCodeFor(classification Classification) int { + return clierrors.ErrorTypeExitCode(string(classification.Type)) +} + +func withClassificationExitCode(err error, classification Classification) error { + if err == nil { + return nil + } + if classification.Type == "" { + return err + } + return clierrors.WithExitCode(err, ExitCodeFor(classification)) +} + +func IsRendered(err error) bool { + var rendered interface{ Rendered() bool } + return errors.As(err, &rendered) && rendered.Rendered() +} + +// Formats lists the accepted --output-format values, in help order. +var Formats = []string{"pretty", "json", "yaml", "table", "toon"} + +func formatErrorBody(body string) string { + var parsed interface{} + if err := json.Unmarshal([]byte(body), &parsed); err != nil { + return body + } + indented, err := json.MarshalIndent(parsed, "", " ") + if err != nil { + return body + } + return string(indented) +} + +// resolveOutputFormat determines the effective output format by checking: +// 0. Explicit --output-format flag +// 1. Environment variable / config file setting (via config.GetString) +// 2. Agent mode default (TOON) +// 3. Default "pretty" +func resolveOutputFormat(cmd *cobra.Command) string { + if format := preparsedRendering.outputFormat; format != "" { + return flagutil.ResolveOutputFormat(cmd, format, IsAgentMode()) + } + if WantsRawResponse(cmd) { + return flagutil.ResolveOutputFormat(cmd, "json", IsAgentMode()) + } + return flagutil.ResolveOutputFormat(cmd, config.GetString("output-format"), IsAgentMode()) +} + +func outputFormatExplicit(cmd *cobra.Command) bool { + if preparsedRendering.outputFormat != "" { + return true + } + if flagutil.FlagChanged(cmd, "output-format") { + return true + } + return config.GetString("output-format") != "" +} + +func jqRawOutput(cmd *cobra.Command) bool { + raw, _ := flagutil.GetBoolFlag(cmd, "raw-output") + return raw +} + +// marshalJSON marshals content to indented JSON bytes. +func marshalJSON(content interface{}) ([]byte, error) { + data, err := utils.MarshalJSON(content, "", true) + if err != nil { + // Fallback for types with non-serializable fields (e.g., func fields) + data, err = json.MarshalIndent(content, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + } + return data, nil +} + +func normalizeForOutput(content interface{}) (interface{}, error) { + data, err := marshalJSON(content) + if err != nil { + return nil, err + } + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + var parsed interface{} + if err := dec.Decode(&parsed); err != nil { + return nil, fmt.Errorf("failed to decode marshalled response: %w", err) + } + return parsed, nil +} + +func convertNumbers(v interface{}, conv func(json.Number) interface{}) interface{} { + switch val := v.(type) { + case json.Number: + return conv(val) + case map[string]interface{}: + for k, child := range val { + val[k] = convertNumbers(child, conv) + } + return val + case []interface{}: + for i, child := range val { + val[i] = convertNumbers(child, conv) + } + return val + } + return v +} + +func nativeNumber(n json.Number) (interface{}, bool) { + if i, err := n.Int64(); err == nil { + return i, true + } + if f, err := n.Float64(); err == nil { + if b, err := json.Marshal(f); err == nil && string(b) == n.String() { + return f, true + } + } + return nil, false +} + +func isIntegerLexeme(lexeme string) bool { + return !strings.ContainsAny(lexeme, ".eE") +} + +func yamlNumber(n json.Number) interface{} { + if v, ok := nativeNumber(n); ok { + return v + } + tag := "!!float" + if isIntegerLexeme(n.String()) { + tag = "!!int" + } + return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: n.String()} +} + +// gotoon encodes every number through float64. +func toonNumber(n json.Number) interface{} { + if v, ok := nativeNumber(n); ok { + return v + } + return n.String() +} + +func marshalYAML(content interface{}) ([]byte, error) { + normalized, err := normalizeForOutput(content) + if err != nil { + return nil, err + } + data, err := yaml.Marshal(convertNumbers(normalized, yamlNumber)) + if err != nil { + return nil, fmt.Errorf("failed to marshal response as YAML: %w", err) + } + return data, nil +} + +func encodeTOON(content interface{}) (string, error) { + normalized, err := normalizeForOutput(content) + if err != nil { + return "", err + } + toonStr, err := gotoon.Encode(convertNumbers(normalized, toonNumber)) + if err != nil { + return "", fmt.Errorf("failed to encode response as TOON: %w", err) + } + return toonStr, nil +} + +// printJSON colorizes (if requested) and prints JSON data followed by a newline. +func printJSON(out io.Writer, data []byte, colorize bool) { + if colorize { + data = ColorizeJSON(data) + } + fmt.Fprintln(out, string(data)) +} + +// WantsRawJSON returns true when the user has requested JSON output format +// (either --output-format=json or --jq is set), meaning the CLI should +// prefer raw JSON passthrough over typed-struct marshaling. +func WantsRawJSON(cmd *cobra.Command) bool { + jqExpr, _ := flagutil.GetStringFlag(cmd, "jq") + if jqExpr != "" { + return true + } + return resolveOutputFormat(cmd) == "json" +} + +func IsMachineMode(cmd *cobra.Command) bool { + if IsAgentMode() { + return true + } + if jqExpr, _ := flagutil.GetStringFlag(cmd, "jq"); jqExpr != "" { + return true + } + format := resolveOutputFormat(cmd) + return format != "pretty" && format != "table" +} + +func renderPathEnvelope(cmd *cobra.Command, absPath string, envelope map[string]interface{}) error { + out := cmd.OutOrStdout() + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + colorize := ShouldColorize(colorFlag) + if jqExpr, _ := flagutil.GetStringFlag(cmd, "jq"); jqExpr != "" { + return applyJqToTyped(out, envelope, jqExpr, colorize, jqRawOutput(cmd)) + } + switch resolveOutputFormat(cmd) { + case "json": + data, err := json.MarshalIndent(envelope, "", " ") + if err != nil { + return err + } + printJSON(out, data, colorize) + case "yaml": + data, err := yaml.Marshal(envelope) + if err != nil { + return fmt.Errorf("failed to marshal file envelope as YAML: %w", err) + } + fmt.Fprint(out, string(data)) + case "toon": + toonStr, err := encodeTOON(envelope) + if err != nil { + return fmt.Errorf("failed to encode file envelope as TOON: %w", err) + } + fmt.Fprint(out, toonStr) + case "table": + return printTable(out, envelope) + default: // pretty + fmt.Fprintln(out, absPath) + } + return nil +} + +// PrepareCallOpts builds common SDK call options from CLI flags. +// Parses --header flags into operations.WithSetHeaders options. +// Server resolution and skip-deserialization are handled per-operation. +func PrepareCallOpts(cmd *cobra.Command) ([]operations.Option, error) { + var opts []operations.Option + if hdrs, _ := flagutil.GetStringArrayFlag(cmd, "header"); len(hdrs) > 0 { + headerMap := make(map[string]string, len(hdrs)) + for _, h := range hdrs { + k, v, ok := strings.Cut(h, ":") + if !ok { + return nil, flagutil.WithCLIValidation(fmt.Errorf("invalid header format %q: expected \"Key: Value\"", h)) + } + headerMap[strings.TrimSpace(k)] = strings.TrimSpace(v) + } + opts = append(opts, operations.WithSetHeaders(headerMap)) + } + return opts, nil +} + +// ValidateGlobalServerIndex validates the --server flag as an integer index +// in range [0, count). Provides clear error messages for invalid values. +// The actual server application happens in client.NewClient via sdk.WithServerIndex +// (which handles server URL template variable resolution). +func ValidateGlobalServerIndex(cmd *cobra.Command, count int) error { + serverFlag, _ := flagutil.GetStringFlag(cmd, "server") + if serverFlag == "" { + return nil + } + idx, err := strconv.Atoi(serverFlag) + if err != nil { + return flagutil.WithCLIValidation(fmt.Errorf("invalid server index %q: must be an integer (0-%d)", serverFlag, count-1)) + } + if idx < 0 || idx >= count { + return flagutil.WithCLIValidation(fmt.Errorf("server index %d out of range (0-%d)", idx, count-1)) + } + return nil +} + +// ValidateGlobalServerName validates the --server flag against a named server map. +// The actual server application happens in client.NewClient via sdk.WithServer. +func ValidateGlobalServerName(cmd *cobra.Command, validNames map[string]string) error { + serverFlag, _ := flagutil.GetStringFlag(cmd, "server") + if serverFlag == "" { + return nil + } + if _, ok := validNames[serverFlag]; !ok { + return flagutil.WithCLIValidation(fmt.Errorf("unknown server %q", serverFlag)) + } + return nil +} + +// Result formats and outputs the response based on the --output-format flag. +func Result(cmd *cobra.Command, res interface{}) error { + if flagutil.DidDryRunRequest(cmd) { + return nil + } + return result(cmd, res, true) +} + +func LocalResult(cmd *cobra.Command, res interface{}) error { + return result(cmd, res, true) +} + +func result(cmd *cobra.Command, res interface{}, allowArtifact bool) error { + out := cmd.OutOrStdout() + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + colorize := ShouldColorize(colorFlag) + jqExpr, _ := flagutil.GetStringFlag(cmd, "jq") + jqRaw := jqRawOutput(cmd) + format := resolveOutputFormat(cmd) + includeHeaders := wantsHeaders(cmd) + + // === BINARY CHECK (must be before raw passthrough) === + // Binary responses (io.ReadCloser, []byte) bypass all formatting. + // This prevents tryReadRawBody from consuming binary as JSON. + if binaryContent := extractResultContent(res); binaryContent != nil { + if rc, ok := binaryContent.(io.ReadCloser); ok { + defer rc.Close() + _, err := io.Copy(out, rc) + return err + } + if b, ok := binaryContent.([]byte); ok { + _, err := out.Write(b) + return err + } + } + + // === ARTIFACT CHECK === + if artifactCfg := activeArtifact(cmd); allowArtifact && artifactCfg != nil { + return artifactResult(cmd, res, artifactCfg) + } + + // === RAW PASSTHROUGH === + // When skip-deserialization was used, the SDK buffered the HTTP response + // body. Read it directly for lossless JSON output, avoiding the + // deserialize-then-reserialize round-trip. + if format == "json" || jqExpr != "" { + rawBody, rawErr := tryReadRawBody(res) + if rawErr != nil { + return rawErr + } + if len(rawBody) > 0 { + if includeHeaders { + headers := extractResponseHeaders(res) + var parsed interface{} + if err := json.Unmarshal(rawBody, &parsed); err == nil { + merged := injectHeaders(parsed, headers) + if jqExpr != "" { + return applyJqToTyped(out, merged, jqExpr, colorize, jqRaw) + } + data, err := json.MarshalIndent(merged, "", " ") + if err != nil { + return err + } + printJSON(out, data, colorize) + return nil + } + // Raw body not valid JSON — wrap as _result_raw string with headers + merged := map[string]interface{}{ + "_result_raw": string(rawBody), + "_response_headers": flattenHeaders(headers), + } + if jqExpr != "" { + return applyJqToTyped(out, merged, jqExpr, colorize, jqRaw) + } + data, err := json.MarshalIndent(merged, "", " ") + if err != nil { + return err + } + printJSON(out, data, colorize) + return nil + } + if jqExpr != "" { + return applyJqToRawJSON(out, rawBody, jqExpr, colorize, jqRaw) + } + printJSON(out, rawBody, colorize) + return nil + } + // Fall through: body was empty (normal deser happened for non-JSON response) + } + + // === TYPED OUTPUT PATH === + content := extractResultContent(res) + if content == nil && !includeHeaders { + return nil + } + + // When --include-headers is set, use the unified header injection path + // for json, yaml, and jq output modes. + if includeHeaders { + headers := extractResponseHeaders(res) + if format == "json" || format == "yaml" || format == "toon" || jqExpr != "" { + return outputWithHeaders(out, content, headers, format, jqExpr, colorize, jqRaw) + } + // Pretty mode: print body content, then headers section + if content != nil { + if err := prettyPrint(out, content, colorize); err != nil { + return err + } + } + printResponseHeadersPretty(out, headers, colorize) + return nil + } + + // If --jq is set, filter through jq and output as JSON (overrides --output-format) + if jqExpr != "" { + return applyJqToTyped(out, content, jqExpr, colorize, jqRaw) + } + + switch format { + case "json": + data, err := marshalJSON(content) + if err != nil { + return err + } + printJSON(out, data, colorize) + case "yaml": + data, err := marshalYAML(content) + if err != nil { + return err + } + fmt.Fprint(out, string(data)) + case "table": + if err := printTable(out, content); err != nil { + return err + } + case "toon": + toonStr, err := encodeTOON(content) + if err != nil { + return err + } + fmt.Fprint(out, toonStr) + default: // "pretty" or unset + if err := prettyPrint(out, content, colorize); err != nil { + return err + } + } + return nil +} + +func Error(cmd *cobra.Command, err error) error { + if err == nil { + return nil + } + format := resolveOutputFormat(cmd) + jqExpr, _ := flagutil.GetStringFlag(cmd, "jq") + classification := Classify(cmd, withErrorOrigin(err, ErrorOriginAPI)) + structured := shouldRenderStructuredError(format, jqExpr) + if shouldClassifyError(format, jqExpr) { + return renderClassifiedError(cmd, err, classification, structured) + } + return renderUnclassifiedAPIError(cmd, err, classification, structured) +} + +func renderClassifiedError(cmd *cobra.Command, err error, classification Classification, structured bool) error { + if IsRendered(err) { + return err + } + if structured { + renderStructuredError(cmd, err, classification) + } else { + renderPrettyError(cmd, err, classification) + } + return markRendered(err, classification) +} + +// renderUnclassifiedAPIError is the compatibility rendering path used when +// classified rendering is not requested: the raw API error text or envelope is +// preserved without the error_type/error_reason taxonomy. +func renderUnclassifiedAPIError(cmd *cobra.Command, err error, classification Classification, structured bool) error { + if IsRendered(err) { + return err + } + if structured { + renderUnclassifiedStructuredAPIError(cmd, err, classification) + } else { + renderUnclassifiedPrettyAPIError(cmd, err, classification) + } + return markRendered(err, classification) +} + +func renderUnclassifiedPrettyAPIError(cmd *cobra.Command, err error, classification Classification) { + out := cmd.ErrOrStderr() + if wantsHeaders(cmd) { + headers := extractErrorResponseHeaders(err) + if len(headers) > 0 { + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + printResponseHeadersPretty(out, headers, ShouldColorize(colorFlag)) + } + } + if classification.RawBody != "" && classification.StatusCode != 0 { + fmt.Fprintf(out, "\nAPI Error (HTTP %d):\n%s\n", classification.StatusCode, formatErrorBody(classification.RawBody)) + } else if classification.RawBody != "" { + fmt.Fprintf(out, "\nAPI Error:\n%s\n", formatErrorBody(classification.RawBody)) + } else { + fmt.Fprintf(out, "\nError: %s\n", err.Error()) + } + if classification.StatusCode == 401 || classification.StatusCode == 403 { + fmt.Fprintf(out, "\nHint: run '%s configure' to set up or update your credentials.\n", cliName) + } +} + +func renderUnclassifiedStructuredAPIError(cmd *cobra.Command, err error, classification Classification) { + statusCode := classification.StatusCode + body := classification.RawBody + var envelope interface{} + if body != "" && isValidJSON(body) { + var parsed interface{} + if json.Unmarshal([]byte(body), &parsed) == nil { + if object, ok := parsed.(map[string]interface{}); ok { + if statusCode != 0 { + if _, exists := object["status_code"]; !exists { + object["status_code"] = statusCode + } + } + if statusCode == 401 || statusCode == 403 { + object["_hint"] = fmt.Sprintf("run '%s configure' to set up or update your credentials", cliName) + } + envelope = object + } else { + wrapped := map[string]interface{}{"error": err.Error(), "body": parsed} + if statusCode != 0 { + wrapped["status_code"] = statusCode + } + if statusCode == 401 || statusCode == 403 { + wrapped["_hint"] = fmt.Sprintf("run '%s configure' to set up or update your credentials", cliName) + } + envelope = wrapped + } + } + } + if envelope == nil { + wrapped := map[string]interface{}{"error": err.Error()} + if statusCode != 0 { + wrapped["status_code"] = statusCode + } + if body != "" { + wrapped["body"] = body + } + if statusCode == 401 || statusCode == 403 { + wrapped["_hint"] = fmt.Sprintf("run '%s configure' to set up or update your credentials", cliName) + } + envelope = wrapped + } + if object, ok := envelope.(map[string]interface{}); ok { + object["exit_code"] = ExitCodeFor(classification) + } + if wantsHeaders(cmd) { + envelope = injectHeaders(envelope, extractErrorResponseHeaders(err)) + } + jsonData, marshalErr := json.MarshalIndent(envelope, "", " ") + if marshalErr != nil { + jsonData, _ = json.MarshalIndent(map[string]interface{}{"error": err.Error(), "exit_code": ExitCodeFor(classification)}, "", " ") + } + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + printJSON(cmd.ErrOrStderr(), jsonData, ShouldColorize(colorFlag)) +} + +func renderPrettyError(cmd *cobra.Command, err error, classification Classification) { + out := cmd.ErrOrStderr() + if wantsHeaders(cmd) { + headers := extractErrorResponseHeaders(err) + if len(headers) > 0 { + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + printResponseHeadersPretty(out, headers, ShouldColorize(colorFlag)) + } + } + fmt.Fprintf(out, "Error (%s): %s\n", classification.Type, classification.Message) + switch { + case classification.Reason != "" && classification.StatusCode != 0: + fmt.Fprintf(out, "Reason: %s (HTTP %d)\n", classification.Reason, classification.StatusCode) + case classification.Reason != "": + fmt.Fprintf(out, "Reason: %s\n", classification.Reason) + case classification.StatusCode != 0: + fmt.Fprintf(out, "HTTP status: %d\n", classification.StatusCode) + } + if len(classification.Hints) > 0 { + fmt.Fprintln(out, "Fix:") + for _, hint := range classification.Hints { + fmt.Fprintf(out, " - %s\n", strings.ReplaceAll(hint, "\n", "\n ")) + } + } + if details := classifiedPrettyDetails(classification); details != "" { + fmt.Fprintln(out, "Details:") + for _, line := range strings.Split(details, "\n") { + fmt.Fprintf(out, " %s\n", line) + } + } +} + +func renderStructuredError(cmd *cobra.Command, err error, classification Classification) { + envelope := classifiedErrorEnvelope(err, classification) + if wantsHeaders(cmd) { + envelope = injectHeaders(envelope, extractErrorResponseHeaders(err)).(map[string]interface{}) + } + jsonData, marshalErr := json.MarshalIndent(envelope, "", " ") + if marshalErr != nil { + jsonData, _ = json.MarshalIndent(map[string]interface{}{ + "error": err.Error(), + "error_type": string(classification.Type), + "exit_code": ExitCodeFor(classification), + "message": classification.Message, + "hints": classification.Hints, + }, "", " ") + } + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + printJSON(cmd.ErrOrStderr(), jsonData, ShouldColorize(colorFlag)) +} + +func classifiedErrorEnvelope(err error, classification Classification) map[string]interface{} { + var envelope map[string]interface{} + if bodyMap, ok := classification.Body.(map[string]interface{}); ok { + envelope = make(map[string]interface{}, len(bodyMap)+5) + for key, value := range bodyMap { + envelope[key] = value + } + } else { + envelope = map[string]interface{}{"error": err.Error()} + if classification.Body != nil { + envelope["body"] = classification.Body + } else if classification.RawBody != "" { + if strings.TrimSpace(classification.RawBody) == "null" { + envelope["body"] = nil + } else { + envelope["body"] = classification.RawBody + } + } + } + delete(envelope, "_hint") + envelope["error_type"] = string(classification.Type) + envelope["exit_code"] = ExitCodeFor(classification) + envelope["message"] = classification.Message + envelope["hints"] = classification.Hints + if classification.Reason != "" { + envelope["error_reason"] = classification.Reason + } else { + delete(envelope, "error_reason") + } + if classification.StatusCode != 0 { + envelope["status_code"] = classification.StatusCode + } else { + delete(envelope, "status_code") + } + mergeMachineErrorFields(envelope, err) + return envelope +} + +func mergeMachineErrorFields(envelope map[string]interface{}, err error) { + var source interface{ MachineErrorFields() map[string]interface{} } + if !errors.As(err, &source) { + return + } + for key, value := range source.MachineErrorFields() { + switch key { + case "error_type", "error_reason", "exit_code", "message", "hints", "status_code": + continue + } + envelope[key] = value + } +} + +func classifiedPrettyDetails(classification Classification) string { + if classification.Body == nil { + if classification.RawBody != "" && classification.RawBody != classification.Message { + return classification.RawBody + } + return "" + } + data, err := json.Marshal(classification.Body) + if err != nil { + return "" + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var cloned interface{} + if err := decoder.Decode(&cloned); err != nil { + return "" + } + stripRenderedErrorFields(cloned, classification.Reason, classification.reasonCarrier, classification.reasonCarrierPath) + cloned = pruneEmptyErrorDetails(cloned) + if cloned == nil { + return "" + } + if bodyMap, ok := cloned.(map[string]interface{}); ok && len(bodyMap) == 1 { + if errorObject, ok := bodyMap["error"].(map[string]interface{}); ok { + cloned = errorObject + } + } + details, err := json.MarshalIndent(cloned, "", " ") + if err != nil || string(details) == "{}" || string(details) == "[]" { + return "" + } + return string(details) +} + +// stripRenderedErrorFields removes only the members already rendered on the +// error header lines: the envelope fields, the error object's message and +// hints, and the reason-carrier member whose value was surfaced on the +// Reason: line. Every other member of the error body is preserved as detail. +func stripRenderedErrorFields(value interface{}, renderedReason string, reasonCarrier []reasonCarrierSegment, reasonCarrierPath []int) { + bodyMap, ok := value.(map[string]interface{}) + if !ok { + return + } + for _, key := range []string{"error_type", "error_reason", "message", "hints", "status_code", "exit_code", "_hint"} { + delete(bodyMap, key) + } + if _, isMessage := bodyMap["error"].(string); isMessage { + delete(bodyMap, "error") + } + errorObject := bodyMap + if nested, ok := bodyMap["error"].(map[string]interface{}); ok { + errorObject = nested + } else if inline, ok := bodyMap["error"].([]interface{}); ok && len(inline) == 1 { + if nested, ok := inline[0].(map[string]interface{}); ok { + errorObject = nested + } + } + for _, key := range []string{"message", "hints"} { + delete(errorObject, key) + } + stripRenderedReasonCarrier(errorObject, renderedReason, reasonCarrier, reasonCarrierPath) +} + +// stripRenderedReasonCarrier deletes, from the carrier that selected the +// rendered reason, the one member whose value was surfaced on the Reason: +// line. wildPath pins that occurrence — the array index the classification +// recorded at each [*] hop — so a sibling array entry sharing the selected +// code, like another declared carrier holding the same string, stays visible +// under Details. A walk that no longer resolves (index out of range, shape +// mismatch, value changed) strips nothing rather than guessing. +func stripRenderedReasonCarrier(errorObject map[string]interface{}, renderedReason string, segments []reasonCarrierSegment, wildPath []int) { + if renderedReason == "" || len(segments) == 0 { + return + } + var parent interface{} = errorObject + for _, segment := range segments[:len(segments)-1] { + if segment.isWild { + items, _ := parent.([]interface{}) + if len(wildPath) == 0 || wildPath[0] < 0 || wildPath[0] >= len(items) { + return + } + parent = items[wildPath[0]] + wildPath = wildPath[1:] + continue + } + object, _ := parent.(map[string]interface{}) + child, ok := object[segment.field] + if !ok { + return + } + parent = child + } + // The carrier grammar guarantees the final segment is a named member. + last := segments[len(segments)-1] + object, _ := parent.(map[string]interface{}) + if reason, ok := object[last.field].(string); ok && reason == renderedReason { + delete(object, last.field) + } +} + +func pruneEmptyErrorDetails(value interface{}) interface{} { + switch typed := value.(type) { + case map[string]interface{}: + for key, child := range typed { + pruned := pruneEmptyErrorDetails(child) + if pruned == nil { + delete(typed, key) + } else { + typed[key] = pruned + } + } + if len(typed) == 0 { + return nil + } + return typed + case []interface{}: + pruned := make([]interface{}, 0, len(typed)) + for _, child := range typed { + if child = pruneEmptyErrorDetails(child); child != nil { + pruned = append(pruned, child) + } + } + if len(pruned) == 0 { + return nil + } + return pruned + default: + return value + } +} + +// StreamEventError is an in-band error event; Body holds that event's JSON. +type StreamEventError struct { + Body string +} + +func (e *StreamEventError) Error() string { + var parsed map[string]interface{} + if json.Unmarshal([]byte(e.Body), &parsed) == nil { + if errObj, ok := parsed["error"].(map[string]interface{}); ok { + if msg, ok := errObj["message"].(string); ok && msg != "" { + return "stream error event: " + msg + } + } + } + return "stream error event: " + e.Body +} + +func tryReadRawBody(res interface{}) ([]byte, error) { + httpRes := extractHTTPResponse(res) + if httpRes == nil || httpRes.Body == nil { + return nil, nil + } + rawBody, err := io.ReadAll(httpRes.Body) + httpRes.Body.Close() + if err != nil { + return nil, fmt.Errorf("reading response body: %w", err) + } + if len(rawBody) == 0 { + return nil, nil + } + return rawBody, nil +} + +func peekRawBody(res interface{}) []byte { + httpRes := extractHTTPResponse(res) + if httpRes == nil || httpRes.Body == nil { + return nil + } + rawBody, err := io.ReadAll(httpRes.Body) + _ = httpRes.Body.Close() + httpRes.Body = io.NopCloser(bytes.NewReader(rawBody)) + if err != nil || len(rawBody) == 0 { + return nil + } + return rawBody +} + +// derefToStruct dereferences pointer/interface values and validates the result +// is a struct. Returns the dereferenced value and true, or a zero Value and false. +func derefToStruct(v reflect.Value) (reflect.Value, bool) { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{}, false + } + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return reflect.Value{}, false + } + return v, true +} + +// extractHTTPResponse extracts the *http.Response from a response envelope. +// Supports envelope-http format (HTTPMeta.Response) and flat format (RawResponse). +func extractHTTPResponse(res interface{}) *http.Response { + if res == nil { + return nil + } + v, ok := derefToStruct(reflect.ValueOf(res)) + if !ok { + return nil + } + // envelope-http: HTTPMeta.Response + httpMeta := v.FieldByName("HTTPMeta") + if httpMeta.IsValid() && httpMeta.Kind() == reflect.Struct { + resp := httpMeta.FieldByName("Response") + if resp.IsValid() && resp.CanInterface() { + if r, ok := resp.Interface().(*http.Response); ok && r != nil { + return r + } + } + } + // flat format fallback: RawResponse + rawResp := v.FieldByName("RawResponse") + if rawResp.IsValid() && rawResp.CanInterface() { + if r, ok := rawResp.Interface().(*http.Response); ok && r != nil { + return r + } + } + return nil +} + +func isStreamResultValue(v reflect.Value) bool { + if !v.IsValid() { + return false + } + for v.Kind() == reflect.Interface { + if v.IsNil() { + return false + } + v = v.Elem() + } + if v.Kind() == reflect.Ptr && v.IsNil() { + return false + } + return v.MethodByName("Next").IsValid() && v.MethodByName("Value").IsValid() +} + +// extractResultContent extracts the meaningful result field from a response envelope. +// SDK response structs contain envelope fields (HTTPMeta or ContentType/StatusCode/RawResponse) +// plus one or more result fields. This function returns the first non-nil result field, +// stripping the envelope so CLI output shows only the data users care about. +func extractResultContent(res interface{}) interface{} { + if res == nil { + return nil + } + root := reflect.ValueOf(res) + if isStreamResultValue(root) { + return nil + } + v, ok := derefToStruct(root) + if !ok { + return res + } + + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + if !field.IsExported() { + continue + } + if isEnvelopeField(field) { + continue + } + fieldVal := v.Field(i) + // Skip function fields (e.g., pagination Next closure) + if fieldVal.Kind() == reflect.Func { + continue + } + // Skip nil pointer/interface fields + if (fieldVal.Kind() == reflect.Ptr || fieldVal.Kind() == reflect.Interface) && fieldVal.IsNil() { + continue + } + if isStreamResultValue(fieldVal) { + continue + } + return fieldVal.Interface() + } + + // No result field found (status-code-only response) + return nil +} + +// isEnvelopeField returns true for standard response envelope fields that should +// be stripped from CLI output. +func isEnvelopeField(field reflect.StructField) bool { + switch field.Type { + case reflect.TypeOf((*http.Response)(nil)): + return true + } + switch field.Name { + case "ContentType", "StatusCode", "HTTPMeta", "Headers": + return true + } + return false +} + +// extractErrorBody extracts the raw response body carried by an error: the +// Body string field fallback SDK errors expose, then the retained HTTP +// response body on typed error models. +func extractErrorBody(err error) string { + for current := err; current != nil; current = errors.Unwrap(current) { + v, ok := derefToStruct(reflect.ValueOf(current)) + if !ok { + continue + } + bodyField := v.FieldByName("Body") + if bodyField.IsValid() && bodyField.Kind() == reflect.String { + return bodyField.String() + } + } + // Typed error models have no Body field but keep the raw server body on + // their retained HTTP response. Prefer it over re-marshaling the typed + // struct (the Error() fallback below), which would drop any fields the + // declared error schema did not capture. + for current := err; current != nil; current = errors.Unwrap(current) { + if body := peekRawBody(current); len(body) > 0 { + return string(body) + } + } + // Streaming-operation SDK errors carry the response body only in their message. + if body := err.Error(); isValidJSON(body) { + return body + } + return "" +} + +// extractErrorStatusCode extracts the StatusCode int field from an error via reflection. +func extractErrorStatusCode(err error) int { + for current := err; current != nil; current = errors.Unwrap(current) { + v, ok := derefToStruct(reflect.ValueOf(current)) + if ok { + field := v.FieldByName("StatusCode") + if field.IsValid() && field.CanInt() { + return int(field.Int()) + } + } + // Streaming-operation SDK errors expose the status only on the embedded response. + if res := extractHTTPResponse(current); res != nil { + return res.StatusCode + } + } + return 0 +} + +func isValidJSON(s string) bool { + var js json.RawMessage + return json.Unmarshal([]byte(s), &js) == nil +} + +// wantsHeaders returns true when --include-headers is set. +func wantsHeaders(cmd *cobra.Command) bool { + includeHeaders, _ := flagutil.GetBoolFlag(cmd, "include-headers") + return includeHeaders +} + +// extractResponseHeaders extracts HTTP response headers from a response envelope. +// Uses extractHTTPResponse to get all HTTP headers (not just spec-defined ones). +func extractResponseHeaders(res interface{}) http.Header { + httpRes := extractHTTPResponse(res) + if httpRes == nil { + return nil + } + return httpRes.Header +} + +// extractErrorResponseHeaders extracts HTTP response headers from an SDK error. +// Error types store *http.Response in a RawResponse field. +func extractErrorResponseHeaders(err error) http.Header { + for current := err; current != nil; current = errors.Unwrap(current) { + v, ok := derefToStruct(reflect.ValueOf(current)) + if !ok { + continue + } + rawResp := v.FieldByName("RawResponse") + if rawResp.IsValid() && rawResp.CanInterface() { + if r, ok := rawResp.Interface().(*http.Response); ok && r != nil { + return r.Header + } + } + } + return nil +} + +// flattenHeaders converts http.Header (map[string][]string) to map[string]interface{} +// for JSON output. Single-value headers are flattened to strings; multi-value headers +// remain as arrays. Key order is not guaranteed; callers relying on deterministic output +// should sort separately (json.Marshal sorts map keys; printResponseHeadersPretty sorts explicitly). +func flattenHeaders(headers http.Header) map[string]interface{} { + if len(headers) == 0 { + return nil + } + result := make(map[string]interface{}, len(headers)) + for k, v := range headers { + if len(v) == 1 { + result[k] = v[0] + } else { + result[k] = v + } + } + return result +} + +// injectHeaders merges flattened response headers into a parsed JSON data structure. +// If data is a map, adds _response_headers as a sibling key. +// If data is nil, returns just the headers. +// Otherwise wraps as {"_result": data, "_response_headers": headers}. +func injectHeaders(data interface{}, headers http.Header) interface{} { + flat := flattenHeaders(headers) + if flat == nil { + return data + } + if data == nil { + return map[string]interface{}{ + "_response_headers": flat, + } + } + if m, ok := data.(map[string]interface{}); ok { + m["_response_headers"] = flat + return m + } + return map[string]interface{}{ + "_result": data, + "_response_headers": flat, + } +} + +// outputWithHeaders marshals content to JSON, parses it, injects headers, +// and outputs in the specified format. This is the common path for --include-headers +// in json, yaml, toon, and jq output modes. +func outputWithHeaders(out io.Writer, content interface{}, headers http.Header, format, jqExpr string, colorize, jqRaw bool) error { + // Marshal content to JSON for a uniform representation + var parsed interface{} + if content != nil { + data, err := marshalJSON(content) + if err != nil { + return err + } + if err := json.Unmarshal(data, &parsed); err != nil { + return err + } + } + + merged := injectHeaders(parsed, headers) + + if jqExpr != "" { + return applyJqToTyped(out, merged, jqExpr, colorize, jqRaw) + } + + switch format { + case "yaml": + yamlData, err := yaml.Marshal(merged) + if err != nil { + return fmt.Errorf("failed to marshal response as YAML: %w", err) + } + fmt.Fprint(out, string(yamlData)) + case "toon": + toonStr, err := encodeTOON(merged) + if err != nil { + return fmt.Errorf("failed to encode response as TOON: %w", err) + } + fmt.Fprint(out, toonStr) + default: // "json" or any other + jsonData, err := json.MarshalIndent(merged, "", " ") + if err != nil { + return err + } + printJSON(out, jsonData, colorize) + } + return nil +} + +// printResponseHeadersPretty prints HTTP response headers in aligned key-value +// format, similar to prettyPrint but as a separate section. +func printResponseHeadersPretty(w io.Writer, headers http.Header, colorize bool) { + if len(headers) == 0 { + return + } + + // Sort header names for stable output + keys := make([]string, 0, len(headers)) + for k := range headers { + keys = append(keys, k) + } + sort.Strings(keys) + + // Calculate max key length for alignment + maxKeyLen := 0 + for _, k := range keys { + if len(k) > maxKeyLen { + maxKeyLen = len(k) + } + } + + fmt.Fprintln(w) + label := "Response Headers" + if colorize { + label = colorKey + label + colorReset + } + fmt.Fprintf(w, "%s:\n", label) + for _, k := range keys { + vals := headers[k] + keyStr := k + if colorize { + keyStr = colorKey + k + colorReset + } + padding := strings.Repeat(" ", maxKeyLen-len(k)) + value := strings.Join(vals, ", ") + if colorize { + value = colorString + value + colorReset + } + fmt.Fprintf(w, " %s:%s %s\n", keyStr, padding, value) + } +} + +func outputJqResults(out io.Writer, results []interface{}, colorize, raw bool) error { + for _, result := range results { + if s, ok := result.(string); ok && raw { + if _, err := fmt.Fprintln(out, s); err != nil { + return err + } + continue + } + data, err := json.MarshalIndent(result, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal jq result: %w", err) + } + printJSON(out, data, colorize) + } + return nil +} + +// applyJqToRawJSON applies a jq expression to raw JSON bytes. +func applyJqToRawJSON(out io.Writer, rawBody []byte, jqExpr string, colorize, raw bool) error { + var data interface{} + if err := json.Unmarshal(rawBody, &data); err != nil { + return fmt.Errorf("failed to parse response JSON for jq: %w", err) + } + results, err := ApplyJqFilter(data, jqExpr) + if err != nil { + return err + } + return outputJqResults(out, results, colorize, raw) +} + +// applyJqToTyped applies a jq expression to typed content (non-raw path). +func applyJqToTyped(out io.Writer, content interface{}, jqExpr string, colorize, raw bool) error { + results, err := ApplyJqFilter(content, jqExpr) + if err != nil { + return err + } + return outputJqResults(out, results, colorize, raw) +} + +// printTable renders content as an aligned table. +// For slices/arrays of structs, each struct becomes a row with struct fields as columns. +// For single structs, outputs a vertical key-value table. +// Complex nested fields are skipped. +func printTable(out io.Writer, content any) error { + v := reflect.ValueOf(content) + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return fmt.Errorf("nil value, nothing to display as table") + } + v = v.Elem() + } + + switch v.Kind() { + case reflect.Slice, reflect.Array: + return printTableRows(out, v) + case reflect.Struct: + return printTableSingle(out, v) + case reflect.Map: + return printTableMap(out, v) + default: + // Fallback: just print the value + fmt.Fprintln(out, v.Interface()) + return nil + } +} + +// printTableRows renders a slice of structs as a multi-row table. +func printTableRows(out io.Writer, v reflect.Value) error { + if v.Len() == 0 { + fmt.Fprintln(out, "(empty)") + return nil + } + + // Collect column info from the first element + first := v.Index(0) + for first.Kind() == reflect.Ptr || first.Kind() == reflect.Interface { + if first.IsNil() { + first = v.Index(0) // use zero value + break + } + first = first.Elem() + } + if first.Kind() != reflect.Struct { + // Non-struct slice: one item per line + for i := 0; i < v.Len(); i++ { + fmt.Fprintln(out, v.Index(i).Interface()) + } + return nil + } + + cols := collectTableColumns(first.Type()) + if len(cols) == 0 { + return fmt.Errorf("no displayable columns found") + } + + tw := newTabWriter(out) + // Header + headers := make([]string, len(cols)) + for i, c := range cols { + headers[i] = strings.ToUpper(c.name) + } + fmt.Fprintln(tw, strings.Join(headers, "\t")) + + // Rows + for i := 0; i < v.Len(); i++ { + row := v.Index(i) + for row.Kind() == reflect.Ptr || row.Kind() == reflect.Interface { + if row.IsNil() { + break + } + row = row.Elem() + } + if row.Kind() != reflect.Struct { + continue + } + vals := make([]string, len(cols)) + for j, c := range cols { + vals[j] = formatTableCell(row.Field(c.index)) + } + fmt.Fprintln(tw, strings.Join(vals, "\t")) + } + return tw.Flush() +} + +// printTableSingle renders a single struct as a vertical key-value table. +func printTableSingle(out io.Writer, v reflect.Value) error { + cols := collectTableColumns(v.Type()) + if len(cols) == 0 { + return fmt.Errorf("no displayable fields found") + } + + tw := newTabWriter(out) + for _, c := range cols { + fmt.Fprintf(tw, "%s\t%s\n", strings.ToUpper(c.name), formatTableCell(v.Field(c.index))) + } + return tw.Flush() +} + +// printTableMap renders a map as a two-column key-value table. +func printTableMap(out io.Writer, v reflect.Value) error { + tw := newTabWriter(out) + fmt.Fprintln(tw, "KEY\tVALUE") + keys := v.MapKeys() + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprint(keys[i].Interface()) < fmt.Sprint(keys[j].Interface()) + }) + for _, k := range keys { + fmt.Fprintf(tw, "%v\t%s\n", k.Interface(), formatTableCell(v.MapIndex(k))) + } + return tw.Flush() +} + +type tableColumn struct { + name string + index int +} + +// collectTableColumns returns the displayable (scalar) fields of a struct type. +func collectTableColumns(t reflect.Type) []tableColumn { + var cols []tableColumn + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.IsExported() { + continue + } + // Skip complex types: structs, slices-of-structs, maps, funcs, chans + ft := f.Type + for ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + switch ft.Kind() { + case reflect.Struct, reflect.Map, reflect.Func, reflect.Chan: + continue + case reflect.Slice, reflect.Array: + // Allow slices of primitives (e.g., []string), skip slices of structs + elem := ft.Elem() + for elem.Kind() == reflect.Ptr { + elem = elem.Elem() + } + if elem.Kind() == reflect.Struct || elem.Kind() == reflect.Map { + continue + } + } + + // Use JSON tag name if available, else field name + name := f.Name + if tag := f.Tag.Get("json"); tag != "" { + parts := strings.Split(tag, ",") + if parts[0] != "" && parts[0] != "-" { + name = parts[0] + } + } + cols = append(cols, tableColumn{name: name, index: i}) + } + return cols +} + +// formatTableCell converts a reflect.Value to a string for table display. +func formatTableCell(v reflect.Value) string { + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + switch v.Kind() { + case reflect.Slice, reflect.Array: + if v.Len() == 0 { + return "" + } + parts := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + parts[i] = fmt.Sprint(v.Index(i).Interface()) + } + return strings.Join(parts, ", ") + case reflect.Bool: + if v.Bool() { + return "true" + } + return "false" + default: + return fmt.Sprint(v.Interface()) + } +} + +func newTabWriter(out io.Writer) *tabwriter.Writer { + return tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) +} diff --git a/internal/output/outputitems.go b/internal/output/outputitems.go new file mode 100644 index 0000000..b92bc78 --- /dev/null +++ b/internal/output/outputitems.go @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package output — incremental item output for pagination and streaming. +// This file is generated when at least one operation uses pagination or streaming. +package output + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +// outputOneItem outputs a single item in the appropriate streaming format. +func outputOneItem(out io.Writer, item interface{}, format, jqExpr string, colorize, jqRaw, first bool) error { + if jqExpr != "" { + results, err := ApplyJqFilter(item, jqExpr) + if err != nil { + return err + } + for _, r := range results { + if s, ok := r.(string); ok && jqRaw { + if _, err := fmt.Fprintln(out, s); err != nil { + return err + } + continue + } + data, err := json.Marshal(r) + if err != nil { + return err + } + printJSON(out, data, colorize) + } + return nil + } + + switch format { + case "json": + // NDJSON: one compact JSON object per line + data, err := marshalJSON(item) + if err != nil { + return err + } + var compacted bytes.Buffer + if err := json.Compact(&compacted, data); err != nil { + // Fallback: output as-is + printJSON(out, data, colorize) + return nil + } + outBytes := compacted.Bytes() + printJSON(out, outBytes, colorize) + case "yaml": + if !first { + fmt.Fprintln(out, "---") + } + data, err := marshalYAML(item) + if err != nil { + return err + } + fmt.Fprint(out, string(data)) + case "toon": + toonStr, err := encodeTOON(item) + if err != nil { + return err + } + if !first { + fmt.Fprintln(out) + } + fmt.Fprint(out, toonStr) + default: // "pretty" + if !first { + fmt.Fprintln(out) // blank line between items + } + if err := prettyPrint(out, item, colorize); err != nil { + return err + } + } + return nil +} diff --git a/internal/output/paginated.go b/internal/output/paginated.go new file mode 100644 index 0000000..a363314 --- /dev/null +++ b/internal/output/paginated.go @@ -0,0 +1,289 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package output — pagination support. +// This file is generated only when at least one operation has pagination. +package output + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" + "github.com/spyzhov/ajson" +) + +// PaginationProbe describes the server-provided continuation value for an operation. +type PaginationProbe struct { + Type string + CursorKind string + NextCursor string + NextURL string + Results string + HasLimit bool +} + +type paginationContinuation struct { + key string + kind string + display string +} + +// PaginatedResult streams all pages from a paginated response, outputting items +// incrementally. contentFieldName is the Go field name of the content on the +// response envelope (e.g., "Res"). resultsFieldName is the Go field path of the +// items slice on the content type (e.g., "ResultArray" or "PageInfo.ResultArray"). +// When resultsFieldName is empty (cursor/URL pagination without explicit results), +// the entire content object is output per page. maxPages limits pagination depth +// (0 = unlimited). +func PaginatedResult(cmd *cobra.Command, res interface{}, contentFieldName, resultsFieldName string, maxPages int, probe PaginationProbe) error { + if flagutil.DidDryRunRequest(cmd) { + return nil + } + ctx := cmd.Context() + out := cmd.OutOrStdout() + format := resolveOutputFormat(cmd) + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + colorize := ShouldColorize(colorFlag) + jqExpr, _ := flagutil.GetStringFlag(cmd, "jq") + jqRaw := jqRawOutput(cmd) + + first := true + pageCount := 0 + seenContinuations := make(map[string]struct{}) + for page := res; page != nil; { + // Check for context cancellation (ctrl+C) + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // Extract content field from response envelope via reflection + v := reflect.ValueOf(page) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + break + } + v = v.Elem() + } + content := extractResultContent(page) + if content != nil { + if resultsFieldName != "" { + // Stream individual items from the results array + contentVal := v.FieldByName(contentFieldName) + if contentVal.IsValid() && !((contentVal.Kind() == reflect.Ptr || contentVal.Kind() == reflect.Interface) && contentVal.IsNil()) { + itemsVal := extractFieldByPath(contentVal, resultsFieldName) + if itemsVal.IsValid() && itemsVal.Kind() == reflect.Slice { + for i := 0; i < itemsVal.Len(); i++ { + item := itemsVal.Index(i).Interface() + if err := outputOneItem(out, item, format, jqExpr, colorize, jqRaw, first); err != nil { + return err + } + first = false + } + } + } + } else { + // No results field path — output the entire content per page + if err := outputOneItem(out, content, format, jqExpr, colorize, jqRaw, first); err != nil { + return err + } + first = false + } + } + + pageCount++ + if maxPages > 0 && pageCount >= maxPages { + if HasMorePages(page, probe) && !IsMachineMode(cmd) { + fmt.Fprintf(cmd.ErrOrStderr(), "Stopped after %d pages (--max-pages); more results may be available.", maxPages) + fmt.Fprintln(cmd.ErrOrStderr()) + } + break + } + + if continuation, ok := extractPaginationContinuation(page, probe); ok { + if _, seen := seenContinuations[continuation.key]; seen { + err := fmt.Errorf("pagination did not advance: the server repeated continuation %s %s", continuation.kind, continuation.display) + return CLIError(cmd, WithCLIReason(err, ReasonCLIProtocol)) + } + seenContinuations[continuation.key] = struct{}{} + } + + // Get next page via SDK's Next() closure + var err error + page, err = callNext(page) + if err != nil { + return Error(cmd, err) + } + } + return nil +} + +func extractPaginationContinuation(res interface{}, probe PaginationProbe) (paginationContinuation, bool) { + rawBody := peekRawBody(res) + if len(rawBody) == 0 { + return paginationContinuation{}, false + } + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return paginationContinuation{}, false + } + if (probe.Type == "cursor" || probe.Type == "url") && probe.Results != "" { + results, err := ajson.Eval(b, probe.Results) + if err != nil || !results.IsArray() { + return paginationContinuation{}, false + } + items, err := results.GetArray() + if err != nil || len(items) == 0 { + return paginationContinuation{}, false + } + } + + switch probe.Type { + case "cursor": + if probe.NextCursor == "" { + return paginationContinuation{}, false + } + node, err := ajson.Eval(b, probe.NextCursor) + if err != nil { + return paginationContinuation{}, false + } + if node.IsNumeric() { + value, err := node.GetNumeric() + if err != nil { + return paginationContinuation{}, false + } + // Key on the value the SDK sends: it truncates numeric cursors for string and integer kinds. + var formatted string + switch probe.CursorKind { + case "string": + formatted = strconv.FormatFloat(value, 'f', 0, 64) + case "integer": + formatted = strconv.FormatInt(int64(value), 10) + default: + formatted = strconv.FormatFloat(value, 'g', -1, 64) + } + return paginationContinuation{ + key: "cursor:number:" + formatted, + kind: "cursor", + display: formatted, + }, true + } + if !node.IsString() { + return paginationContinuation{}, false + } + value, err := node.GetString() + if err != nil || strings.TrimSpace(value) == "" { + return paginationContinuation{}, false + } + return paginationContinuation{ + key: "cursor:string:" + value, + kind: "cursor", + display: strconv.Quote(value), + }, true + + case "url": + if probe.NextURL == "" { + return paginationContinuation{}, false + } + node, err := ajson.Eval(b, probe.NextURL) + if err != nil || !node.IsString() { + return paginationContinuation{}, false + } + value, err := node.GetString() + if err != nil || value == "" { + return paginationContinuation{}, false + } + return paginationContinuation{ + key: "url:" + value, + kind: "URL", + display: strconv.Quote(value), + }, true + } + + return paginationContinuation{}, false +} + +// callNext invokes the Next() pagination closure on a response envelope via reflection. +// Returns (nextRes, nil) on success, (nil, nil) when pagination is complete, or (nil, err) on failure. +func callNext(res interface{}) (interface{}, error) { + v := reflect.ValueOf(res) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil, nil + } + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return nil, nil + } + nextField := v.FieldByName("Next") + if !nextField.IsValid() || nextField.Kind() != reflect.Func || nextField.IsNil() { + return nil, nil + } + // Next() signature is func() (*T, error) + results := nextField.Call(nil) + if len(results) != 2 { + return nil, fmt.Errorf("unexpected Next() return count: %d", len(results)) + } + if !results[1].IsNil() { + if err, ok := results[1].Interface().(error); ok { + return nil, err + } + return nil, fmt.Errorf("Next() returned non-error second value: %v", results[1].Interface()) + } + if results[0].IsNil() { + return nil, nil + } + return results[0].Interface(), nil +} + +// HasMorePages never calls Next; offset/limit probes always report false. +func HasMorePages(res interface{}, probe PaginationProbe) bool { + // Next also compares the results length with the runtime limit, which the probe lacks. + if probe.Results != "" && probe.HasLimit { + return false + } + _, ok := extractPaginationContinuation(res, probe) + return ok +} + +// extractFieldByPath navigates a reflect.Value by dot-delimited Go field names. +// Returns an invalid reflect.Value if any segment is not found or nil. +func extractFieldByPath(v reflect.Value, path string) reflect.Value { + parts := strings.Split(path, ".") + current := v + for _, part := range parts { + for current.Kind() == reflect.Ptr || current.Kind() == reflect.Interface { + if current.IsNil() { + return reflect.Value{} + } + current = current.Elem() + } + if current.Kind() != reflect.Struct { + return reflect.Value{} + } + current = current.FieldByName(part) + if !current.IsValid() { + return reflect.Value{} + } + } + return current +} diff --git a/internal/output/pretty.go b/internal/output/pretty.go new file mode 100644 index 0000000..1850c0a --- /dev/null +++ b/internal/output/pretty.go @@ -0,0 +1,263 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package output + +import ( + "encoding/json" + "fmt" + "io" + "sort" + "strings" +) + +// prettyPrint writes content in a human-readable key-value format. +// For objects it produces aligned "key: value" pairs; for arrays of objects +// it prints numbered entries. Nested structures use 2-space indentation. +func prettyPrint(w io.Writer, content interface{}, colorize bool) error { + parsed, err := normalizeForOutput(content) + if err != nil { + return err + } + + pp := &prettyPrinter{w: w, colorize: colorize} + pp.printValue(parsed, 0) + return pp.err +} + +type prettyPrinter struct { + w io.Writer + colorize bool + err error +} + +func (p *prettyPrinter) write(s string) { + if p.err != nil { + return + } + _, p.err = io.WriteString(p.w, s) +} + +func (p *prettyPrinter) writef(format string, args ...interface{}) { + if p.err != nil { + return + } + _, p.err = fmt.Fprintf(p.w, format, args...) +} + +func (p *prettyPrinter) colorStr(color, s string) string { + if p.colorize { + return color + s + colorReset + } + return s +} + +func (p *prettyPrinter) printValue(v interface{}, indent int) { + switch val := v.(type) { + case map[string]interface{}: + p.printObject(val, indent) + case []interface{}: + p.printArray(val, indent) + case string: + p.write(p.colorStr(colorString, val)) + p.write("\n") + case float64: + formatted := formatNumber(val) + p.write(p.colorStr(colorNumber, formatted)) + p.write("\n") + case json.Number: + p.write(p.colorStr(colorNumber, formatJSONNumber(val))) + p.write("\n") + case bool: + p.write(p.colorStr(colorBool, fmt.Sprintf("%v", val))) + p.write("\n") + case nil: + p.write(p.colorStr(colorNull, "null")) + p.write("\n") + default: + p.writef("%v\n", val) + } +} + +func (p *prettyPrinter) printObject(obj map[string]interface{}, indent int) { + if len(obj) == 0 { + p.write("{}\n") + return + } + + // Sort keys for stable output + keys := make([]string, 0, len(obj)) + for k := range obj { + keys = append(keys, k) + } + sort.Strings(keys) + + // Calculate max key length for alignment + maxKeyLen := 0 + for _, k := range keys { + if len(k) > maxKeyLen { + maxKeyLen = len(k) + } + } + + prefix := strings.Repeat(" ", indent) + for _, k := range keys { + val := obj[k] + keyStr := p.colorStr(colorKey, k) + + switch child := val.(type) { + case map[string]interface{}: + p.writef("%s%s:\n", prefix, keyStr) + p.printObject(child, indent+1) + case []interface{}: + p.writef("%s%s:\n", prefix, keyStr) + p.printArray(child, indent+1) + default: + padding := strings.Repeat(" ", maxKeyLen-len(k)) + p.writef("%s%s:%s ", prefix, keyStr, padding) + p.printInlineValue(val) + p.write("\n") + } + } +} + +func (p *prettyPrinter) printArray(arr []interface{}, indent int) { + if len(arr) == 0 { + p.write("[]\n") + return + } + + prefix := strings.Repeat(" ", indent) + + // Check if all elements are simple scalars + allScalar := true + for _, item := range arr { + switch item.(type) { + case map[string]interface{}, []interface{}: + allScalar = false + } + if !allScalar { + break + } + } + + if allScalar { + for _, item := range arr { + p.writef("%s- ", prefix) + p.printInlineValue(item) + p.write("\n") + } + return + } + + // Complex array: print each item with a separator for objects + for i, item := range arr { + switch child := item.(type) { + case map[string]interface{}: + if i > 0 { + p.write("\n") + } + p.writef("%s- ", prefix) + p.printCompactObject(child, indent+1, true) + case []interface{}: + p.writef("%s-\n", prefix) + p.printArray(child, indent+1) + default: + p.writef("%s- ", prefix) + p.printInlineValue(item) + p.write("\n") + } + } +} + +// printCompactObject prints an object inside an array entry. +// The first key is printed on the same line as "- ", subsequent keys are indented. +func (p *prettyPrinter) printCompactObject(obj map[string]interface{}, indent int, firstInline bool) { + keys := make([]string, 0, len(obj)) + for k := range obj { + keys = append(keys, k) + } + sort.Strings(keys) + + maxKeyLen := 0 + for _, k := range keys { + if len(k) > maxKeyLen { + maxKeyLen = len(k) + } + } + + prefix := strings.Repeat(" ", indent) + for i, k := range keys { + val := obj[k] + keyStr := p.colorStr(colorKey, k) + + linePrefix := prefix + if i == 0 && firstInline { + linePrefix = "" // first key goes on same line as "- " + } + + switch child := val.(type) { + case map[string]interface{}: + p.writef("%s%s:\n", linePrefix, keyStr) + p.printObject(child, indent+1) + case []interface{}: + p.writef("%s%s:\n", linePrefix, keyStr) + p.printArray(child, indent+1) + default: + padding := strings.Repeat(" ", maxKeyLen-len(k)) + p.writef("%s%s:%s ", linePrefix, keyStr, padding) + p.printInlineValue(val) + p.write("\n") + } + } +} + +// printInlineValue prints a scalar value on the current line (no trailing newline). +func (p *prettyPrinter) printInlineValue(v interface{}) { + switch val := v.(type) { + case string: + p.write(p.colorStr(colorString, val)) + case float64: + p.write(p.colorStr(colorNumber, formatNumber(val))) + case json.Number: + p.write(p.colorStr(colorNumber, formatJSONNumber(val))) + case bool: + p.write(p.colorStr(colorBool, fmt.Sprintf("%v", val))) + case nil: + p.write(p.colorStr(colorNull, "null")) + default: + p.writef("%v", val) + } +} + +// formatNumber formats a float64, printing integers without a decimal point. +func formatNumber(f float64) string { + if f == float64(int64(f)) { + return fmt.Sprintf("%d", int64(f)) + } + return fmt.Sprintf("%g", f) +} + +func formatJSONNumber(n json.Number) string { + // The integer form is used only when it reproduces the wire lexeme: + // Int64 canonicalizes values like -0 or 1e2, which must print verbatim. + if i, err := n.Int64(); err == nil { + if formatted := fmt.Sprintf("%d", i); formatted == n.String() { + return formatted + } + } + return n.String() +} diff --git a/internal/output/streaming.go b/internal/output/streaming.go new file mode 100644 index 0000000..0c51a15 --- /dev/null +++ b/internal/output/streaming.go @@ -0,0 +1,238 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package output — streaming support (SSE EventStream, JSONL/NDJSON). +// This file is generated only when at least one operation has a streaming response. +package output + +import ( + "encoding/json" + "errors" + "reflect" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" +) + +// StreamResult iterates a streaming response (SSE EventStream or JSONL/NDJSON +// JsonLStream) and outputs each event incrementally. streamFieldName is the Go +// field name on the response envelope that holds the stream (e.g., "EventStream", +// "Object"). The function uses reflection to call Next()/Value()/Err()/Close() +// on the stream field, handling both EventStream (Value() *T) and JsonLStream +// (Value() (T, error)) signatures. +func StreamResult(cmd *cobra.Command, res interface{}, streamFieldName string) error { + if flagutil.DidDryRunRequest(cmd) { + return nil + } + ctx := cmd.Context() + out := cmd.OutOrStdout() + format := resolveOutputFormat(cmd) + colorFlag, _ := flagutil.GetStringFlag(cmd, "color") + colorize := ShouldColorize(colorFlag) + jqExpr, _ := flagutil.GetStringFlag(cmd, "jq") + jqRaw := jqRawOutput(cmd) + + // Extract the stream field from the response envelope via reflection + v := reflect.ValueOf(res) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + v = v.Elem() + } + if v.Kind() != reflect.Struct { + // Fallback: not a struct envelope — output as regular result + return Result(cmd, res) + } + streamVal := v.FieldByName(streamFieldName) + if !streamVal.IsValid() { + // Fallback: stream field not found — output as regular result + return Result(cmd, res) + } + // Guard IsNil: only call on nillable kinds. Fall back to Result() for + // mixed-content operations where non-stream content may still exist. + if isNillable(streamVal) && streamVal.IsNil() { + return Result(cmd, res) + } + + // Validate streaming interface: must have Next and Value methods + nextMethod := streamVal.MethodByName("Next") + valueMethod := streamVal.MethodByName("Value") + errMethod := streamVal.MethodByName("Err") + closeMethod := streamVal.MethodByName("Close") + if !nextMethod.IsValid() || !valueMethod.IsValid() { + // Fallback: not a streaming type — output as regular result + return Result(cmd, res) + } + + if closeMethod.IsValid() { + defer closeMethod.Call(nil) + } + + projector := newStreamProjector(cmd) + + // Projected output already written must end with its terminating + // newline before any error is rendered. The primary error keeps the + // exit classification; a failed finish is joined rather than swallowed. + finishJoin := func(primary error) error { + if projector != nil { + if finishErr := projector.finish(out); finishErr != nil { + return errors.Join(primary, finishErr) + } + } + return primary + } + + first := true + for { + // Check for context cancellation (Ctrl+C) + select { + case <-ctx.Done(): + return finishJoin(ctx.Err()) + default: + } + + // Call Next() bool + nextResults := nextMethod.Call(nil) + if !nextResults[0].Bool() { + break + } + + // Call Value() — handles both: + // EventStream: Value() *T (1 return value) + // JsonLStream: Value() (T, error) (2 return values) + valueResults := valueMethod.Call(nil) + var item interface{} + if len(valueResults) == 2 { + // JsonLStream: check error — route through Error() for JSON formatting + if !valueResults[1].IsNil() { + return finishJoin(Error(cmd, valueResults[1].Interface().(error))) + } + item = valueResults[0].Interface() + } else { + // EventStream: dereference pointer if needed + val := valueResults[0] + if val.Kind() == reflect.Ptr && !val.IsNil() { + item = val.Elem().Interface() + } else if val.Kind() == reflect.Ptr && val.IsNil() { + continue // skip nil events + } else { + item = val.Interface() + } + } + + if len(valueResults) == 1 { + if body, isErr := streamErrorEvent(item); isErr { + return finishJoin(Error(cmd, &StreamEventError{Body: string(body)})) + } + } + + if projector != nil { + if err := projector.emit(out, item); err != nil { + return finishJoin(err) + } + } else if err := outputOneItem(out, item, format, jqExpr, colorize, jqRaw, first); err != nil { + return err + } + first = false + } + + // Check for stream errors — route through Error() for consistent + // --output-format json behavior + if errMethod.IsValid() { + errResults := errMethod.Call(nil) + if len(errResults) > 0 && !errResults[0].IsNil() { + return finishJoin(Error(cmd, errResults[0].Interface().(error))) + } + } + + if projector != nil { + return projector.finish(out) + } + return nil +} + +func streamErrorEvent(item interface{}) (json.RawMessage, bool) { + u, ok := errorUnionValue(reflect.ValueOf(item)) + if !ok { + return nil, false + } + data, err := marshalJSON(u.Interface()) + if err != nil { + return nil, false + } + return data, true +} + +func errorUnionValue(v reflect.Value) (reflect.Value, bool) { + v = derefValue(v) + if v.Kind() != reflect.Struct { + return reflect.Value{}, false + } + if isUnionStruct(v.Type()) { + if unionNamesErrorVariant(v) { + return v, true + } + return reflect.Value{}, false + } + data := derefValue(v.FieldByName("Data")) + if data.Kind() == reflect.Struct && isUnionStruct(data.Type()) && unionNamesErrorVariant(data) { + return data, true + } + return reflect.Value{}, false +} + +func derefValue(v reflect.Value) reflect.Value { + for v.IsValid() && (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + return v +} + +func isUnionStruct(t reflect.Type) bool { + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Tag.Get("union") == "member" { + return true + } + } + return false +} + +func unionNamesErrorVariant(u reflect.Value) bool { + typeField := u.FieldByName("Type") + return typeField.IsValid() && typeField.Kind() == reflect.String && isErrorDiscriminator(typeField.String()) +} + +// Case-sensitive: undiscriminated unions put PascalCase variant names ("ErrorEvent") in Type. +func isErrorDiscriminator(value string) bool { + if value == "error" { + return true + } + return strings.HasSuffix(value, ".error") || strings.HasSuffix(value, "_error") || strings.HasSuffix(value, "-error") +} + +// isNillable returns true if IsNil() can be safely called on v. +func isNillable(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr, reflect.Interface, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan: + return true + } + return false +} diff --git a/internal/output/streamproject.go b/internal/output/streamproject.go new file mode 100644 index 0000000..6a91e75 --- /dev/null +++ b/internal/output/streamproject.go @@ -0,0 +1,164 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +// Package output — streamed-event projection (x-speakeasy-cli-commands output.stream.select). +package output + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/flagutil" + "github.com/spf13/cobra" +) + +const streamSelectAnnotation = "speakeasy_stream_select" + +type streamProjector struct { + pointer []string + select_ string + wrote bool + lastByte byte +} + +func newStreamProjector(cmd *cobra.Command) *streamProjector { + if cmd == nil || cmd.Annotations == nil { + return nil + } + pointer, ok := cmd.Annotations[streamSelectAnnotation] + if !ok || pointer == "" { + return nil + } + if flagutil.FlagChanged(cmd, "jq") { + return nil + } + if outputFormatExplicit(cmd) { + return nil + } + // --raw-response asks for complete events; the declared projection must + // not strip them down to the selected field. The flag is read directly + // because it is registered by the artifact runtime, which may not be + // generated for this CLI at all. + if rawResponse, _ := flagutil.GetBoolFlag(cmd, "raw-response"); rawResponse { + return nil + } + return &streamProjector{pointer: splitJSONPointer(pointer), select_: pointer} +} + +func splitJSONPointer(pointer string) []string { + if pointer == "" { + return nil + } + parts := strings.Split(strings.TrimPrefix(pointer, "/"), "/") + for i, part := range parts { + part = strings.ReplaceAll(part, "~1", "/") + parts[i] = strings.ReplaceAll(part, "~0", "~") + } + return parts +} + +func (p *streamProjector) emit(out io.Writer, item interface{}) error { + data, err := marshalJSON(item) + if err != nil { + return err + } + var decoded interface{} + if err := json.Unmarshal(data, &decoded); err != nil { + return fmt.Errorf("stream projection: decode event: %w", err) + } + value, found := walkJSONPointer(decoded, p.pointer) + if !found || value == nil { + return nil + } + text, ok := value.(string) + if !ok { + return fmt.Errorf("stream projection %s selected %s, not a string; the selected field must be a string in every event kind that carries it", p.select_, jsonKind(value)) + } + if text == "" { + return nil + } + return p.write(out, []byte(text)) +} + +func (p *streamProjector) finish(out io.Writer) error { + if p == nil || !p.wrote || p.lastByte == '\n' { + return nil + } + return p.write(out, []byte{'\n'}) +} + +func (p *streamProjector) write(out io.Writer, chunk []byte) error { + if len(chunk) == 0 { + return nil + } + n, err := out.Write(chunk) + if err != nil { + return err + } + if n != len(chunk) { + return io.ErrShortWrite + } + p.wrote = true + p.lastByte = chunk[len(chunk)-1] + if flusher, ok := out.(interface{ Flush() error }); ok { + return flusher.Flush() + } + return nil +} + +func walkJSONPointer(value interface{}, tokens []string) (interface{}, bool) { + current := value + for _, token := range tokens { + switch node := current.(type) { + case map[string]interface{}: + next, ok := node[token] + if !ok { + return nil, false + } + current = next + case []interface{}: + idx, err := strconv.Atoi(token) + if err != nil || idx < 0 || idx >= len(node) { + return nil, false + } + current = node[idx] + default: + return nil, false + } + } + return current, true +} + +func jsonKind(value interface{}) string { + switch value.(type) { + case nil: + return "null" + case bool: + return "a boolean" + case float64, json.Number: + return "a number" + case string: + return "a string" + case []interface{}: + return "an array" + case map[string]interface{}: + return "an object" + } + return fmt.Sprintf("%T", value) +} diff --git a/internal/sdk/CONTRIBUTING.md b/internal/sdk/CONTRIBUTING.md new file mode 100644 index 0000000..d585717 --- /dev/null +++ b/internal/sdk/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing to This Repository + +Thank you for your interest in contributing to this repository. Please note that this repository contains generated code. As such, we do not accept direct changes or pull requests. Instead, we encourage you to follow the guidelines below to report issues and suggest improvements. + +## How to Report Issues + +If you encounter any bugs or have suggestions for improvements, please open an issue on GitHub. When reporting an issue, please provide as much detail as possible to help us reproduce the problem. This includes: + +- A clear and descriptive title +- Steps to reproduce the issue +- Expected and actual behavior +- Any relevant logs, screenshots, or error messages +- Information about your environment (e.g., operating system, software versions) + - For example can be collected using the `npx envinfo` command from your terminal if you have Node.js installed + +## Issue Triage and Upstream Fixes + +We will review and triage issues as quickly as possible. Our goal is to address bugs and incorporate improvements in the upstream source code. Fixes will be included in the next generation of the generated code. + +## Contact + +If you have any questions or need further assistance, please feel free to reach out by opening an issue. + +Thank you for your understanding and cooperation! + +The Maintainers diff --git a/internal/sdk/agent.go b/internal/sdk/agent.go new file mode 100644 index 0000000..4bb4b41 --- /dev/null +++ b/internal/sdk/agent.go @@ -0,0 +1,1795 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/agents" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types/stream" + "github.com/spyzhov/ajson" +) + +// Agent - Run interactions with Gemini models or managed agents, and manage agent definitions. +// +// Start here: +// +// gemini-api agent run --help +// +// Other common flows: +// +// gemini-api agent create --help Define a managed agent +// gemini-api agent status --help Inspect a background interaction +// +// Note: agent IDs and interaction IDs are distinct resources. "agent status" +// takes an interaction ID; to inspect an agent definition use "agent get". +type Agent struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newAgent(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Agent { + return &Agent{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// List managed agent definitions +// Lists all Agents. +func (s *Agent) List(ctx context.Context, request *operations.ListAgentsRequest, opts ...operations.Option) (*operations.ListAgentsResponse, error) { + globals := operations.ListAgentsGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/agents", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ListAgents", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + paginationCtx := ctx + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ListAgentsResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + res.Next = func() (*operations.ListAgentsResponse, error) { + if request == nil { + request = &operations.ListAgentsRequest{} + } + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return nil, err + } + nC, err := ajson.Eval(b, "$.next_page_token") + if err != nil { + return nil, err + } + var nCVal string + + if nC.IsNumeric() { + numVal, err := nC.GetNumeric() + if err != nil { + return nil, err + } + // GetNumeric returns as float64 so convert to the appropriate type. + nCVal = strconv.FormatFloat(numVal, 'f', 0, 64) + } else { + val, err := nC.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + nCVal = val.(string) + if strings.TrimSpace(nCVal) == "" { + return nil, nil + } + } + request.PageToken = &nCVal + + return s.List( + paginationCtx, + request, + opts..., + ) + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out agents.AgentListResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.AgentListResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Create a managed agent definition +// Creates a new Agent (Typed version for SDK). +func (s *Agent) Create(ctx context.Context, request operations.CreateAgentRequest, opts ...operations.Option) (*operations.CreateAgentResponse, error) { + globals := operations.CreateAgentGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/agents", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "CreateAgent", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CreateAgentResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out agents.Agent + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Agent = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Delete a managed agent definition by ID +// Deletes an Agent. +func (s *Agent) Delete(ctx context.Context, request operations.DeleteAgentRequest, opts ...operations.Option) (*operations.DeleteAgentResponse, error) { + globals := operations.DeleteAgentGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/agents/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "DeleteAgent", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteAgentResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Empty + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Empty = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Get a managed agent definition by ID +// Gets a specific Agent. +func (s *Agent) Get(ctx context.Context, request operations.GetAgentRequest, opts ...operations.Option) (*operations.GetAgentResponse, error) { + globals := operations.GetAgentGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/agents/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "GetAgent", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetAgentResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out agents.Agent + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Agent = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Run an interaction with a Gemini model or a managed agent +// Run one interaction with either a Gemini model or an existing managed agent. +// Provide input and choose exactly one selector: "model" or "agent". +// +// For a first model run, use gemini-3.6-flash (recommended starting model). +// Other model IDs: https://ai.google.dev/gemini-api/docs/models +// +// Pass the full JSON with --body or stdin. Set "background": true to +// return immediately with an interaction ID, then poll: +// +// gemini-api agent status --id +func (s *Agent) Run(ctx context.Context, request operations.CreateInteractionRequest, opts ...operations.Option) (*operations.CreateInteractionResponse, error) { + globals := operations.CreateInteractionGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionAcceptHeaderOverride, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/interactions", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "CreateInteraction", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + if o.AcceptHeaderOverride != nil { + req.Header.Set("Accept", string(*o.AcceptHeaderOverride)) + } else { + req.Header.Set("Accept", "application/json;q=1, text/event-stream;q=0") + } + + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CreateInteractionResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Interaction + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Interaction = &out + } + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `text/event-stream`): + out := stream.NewEventStream(ctx, httpRes.Body, func(se []byte) (interactions.InteractionSSEStreamEvent, error) { + var e interactions.InteractionSSEStreamEvent + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(se), &e, ""); err != nil { + return interactions.InteractionSSEStreamEvent{}, err + } + return e, nil + }, "[DONE]", stream.WithCancel[interactions.InteractionSSEStreamEvent](streamCancel)) + streamCancel = nil + res.InteractionSSEStreamEvent = out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.CreateInteractionClientError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.CreateInteractionServerError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// DeleteInteraction - Delete an interaction by interaction ID +// Deletes the interaction by id. +func (s *Agent) DeleteInteraction(ctx context.Context, request operations.DeleteInteractionRequest, opts ...operations.Option) (*operations.DeleteInteractionResponse, error) { + globals := operations.DeleteInteractionGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/interactions/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "deleteInteraction", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteInteractionResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + if o.SkipDeserialization == nil || !*o.SkipDeserialization { + utils.DrainBody(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.DeleteInteractionClientError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.DeleteInteractionServerError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Status - Get status and output of an interaction by interaction ID +// Get the status and output of an interaction by interaction ID. Use this to poll a background run started with "agent run". +func (s *Agent) Status(ctx context.Context, request operations.GetInteractionByIDRequest, opts ...operations.Option) (*operations.GetInteractionByIDResponse, error) { + globals := operations.GetInteractionByIDGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionAcceptHeaderOverride, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/interactions/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "getInteractionById", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + if o.AcceptHeaderOverride != nil { + req.Header.Set("Accept", string(*o.AcceptHeaderOverride)) + } else { + req.Header.Set("Accept", "application/json;q=1, text/event-stream;q=0") + } + + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetInteractionByIDResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Interaction + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Interaction = &out + } + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `text/event-stream`): + out := stream.NewEventStream(ctx, httpRes.Body, func(se []byte) (interactions.InteractionSSEStreamEvent, error) { + var e interactions.InteractionSSEStreamEvent + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(se), &e, ""); err != nil { + return interactions.InteractionSSEStreamEvent{}, err + } + return e, nil + }, "[DONE]", stream.WithCancel[interactions.InteractionSSEStreamEvent](streamCancel)) + streamCancel = nil + res.InteractionSSEStreamEvent = out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.GetInteractionByIDClientError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.GetInteractionByIDServerError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Cancel an in-progress interaction by interaction ID +// Cancels an interaction by id. This only applies to background interactions that are still running. +func (s *Agent) Cancel(ctx context.Context, request operations.CancelInteractionByIDRequest, opts ...operations.Option) (*operations.CancelInteractionByIDResponse, error) { + globals := operations.CancelInteractionByIDGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/interactions/{id}/cancel", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "cancelInteractionById", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CancelInteractionByIDResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Interaction + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Interaction = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.CancelInteractionByIDClientError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out sdkerrors.CancelInteractionByIDServerError + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + out.HTTPMeta = components.HTTPMetadata{ + Request: req, + Response: httpRes, + } + return nil, &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/credentials.go b/internal/sdk/credentials.go new file mode 100644 index 0000000..73f70f8 --- /dev/null +++ b/internal/sdk/credentials.go @@ -0,0 +1,1009 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/credentials" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type Credentials struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newCredentials(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Credentials { + return &Credentials{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// List - Lists credentials for a project. +func (s *Credentials) List(ctx context.Context, request *operations.ListCredentialsRequest, opts ...operations.Option) (*operations.ListCredentialsResponse, error) { + globals := operations.ListCredentialsGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/credentials", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ListCredentials", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ListCredentialsResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out credentials.CredentialListResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.CredentialListResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Create - Creates a credential. +func (s *Credentials) Create(ctx context.Context, request operations.CreateCredentialRequest, opts ...operations.Option) (*operations.CreateCredentialResponse, error) { + globals := operations.CreateCredentialGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/credentials", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "CreateCredential", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CreateCredentialResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out credentials.Credential + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Credential = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Delete - Deletes a credential. Fails if referenced by active triggers. +func (s *Credentials) Delete(ctx context.Context, request operations.DeleteCredentialRequest, opts ...operations.Option) (*operations.DeleteCredentialResponse, error) { + globals := operations.DeleteCredentialGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/credentials/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "DeleteCredential", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteCredentialResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Empty + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Empty = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Get - Gets metadata of a single credential (no secret fields). +func (s *Credentials) Get(ctx context.Context, request operations.GetCredentialRequest, opts ...operations.Option) (*operations.GetCredentialResponse, error) { + globals := operations.GetCredentialGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/credentials/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "GetCredential", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetCredentialResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out credentials.Credential + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Credential = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Update - Updates a credential. +func (s *Credentials) Update(ctx context.Context, request operations.UpdateCredentialRequest, opts ...operations.Option) (*operations.UpdateCredentialResponse, error) { + globals := operations.UpdateCredentialGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/credentials/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "UpdateCredential", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "PATCH", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.UpdateCredentialResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out credentials.Credential + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Credential = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/docs/models/operations/option.md b/internal/sdk/docs/models/operations/option.md new file mode 100644 index 0000000..32e745d --- /dev/null +++ b/internal/sdk/docs/models/operations/option.md @@ -0,0 +1,179 @@ +# Options + +## Global Options + +Global options are passed when initializing the SDK client and apply to all operations. + +### WithServerURL + +WithServerURL allows providing an alternative server URL. + +```go +sdk.WithServerURL("https://api.example.com") +``` + +### WithTemplatedServerURL + +WithTemplatedServerURL allows providing an alternative server URL with templated parameters. + +```go +sdk.WithTemplatedServerURL("https://{host}:{port}", map[string]string{ + "host": "api.example.com", + "port": "8080", +}) +``` + +### WithServerIndex + +WithServerIndex allows the overriding of the default server by index. + +```go +sdk.WithServerIndex(1) +``` + +### WithClient + +WithClient allows the overriding of the default HTTP client used by the SDK. + +```go +sdk.WithClient(httpClient) +``` + +### WithSecurity + +WithSecurity configures the SDK to use the provided security details. + +```go +sdk.WithSecurity(/* ... */) +``` + +### WithSecuritySource + +WithSecuritySource configures the SDK to invoke the provided function on each method call to determine authentication. + +```go +sdk.WithSecuritySource(/* ... */) +``` + +### WithAPIVersion + +WithAPIVersion allows setting the APIVersion parameter for all supported operations. + +```go +sdk.WithAPIVersion(/* ... */) +``` + +### WithAPIRevision + +WithAPIRevision allows setting the APIRevision parameter for all supported operations. + +```go +sdk.WithAPIRevision(/* ... */) +``` + +### WithUserProject + +WithUserProject allows setting the UserProject parameter for all supported operations. + +```go +sdk.WithUserProject(/* ... */) +``` + +### WithRetryConfig + +WithRetryConfig allows setting the default retry configuration used by the SDK for all supported operations. + +```go +sdk.WithRetryConfig(retry.Config{ + Strategy: "backoff", + Backoff: retry.BackoffStrategy{ + InitialInterval: 500 * time.Millisecond, + MaxInterval: 60 * time.Second, + Exponent: 1.5, + MaxElapsedTime: 5 * time.Minute, + }, + RetryConnectionErrors: true, +}) +``` + +### WithTimeout + +WithTimeout sets the default request timeout for all operations. + +```go +sdk.WithTimeout(30 * time.Second) +``` + +## Per-Method Options + +Per-method options are passed as the last argument to individual methods and override any global settings for that request. + +### WithServerURL + +WithServerURL allows providing an alternative server URL for a single request. + +```go +operations.WithServerURL("http://api.example.com") +``` + +### WithTemplatedServerURL + +WithTemplatedServerURL allows providing an alternative server URL with templated parameters for a single request. + +```go +operations.WithTemplatedServerURL("http://{host}:{port}", map[string]string{ + "host": "api.example.com", + "port": "8080", +}) +``` + +### WithRetries + +WithRetries allows customizing the default retry configuration for a single request. + +```go +operations.WithRetries(retry.Config{ + Strategy: "backoff", + Backoff: retry.BackoffStrategy{ + InitialInterval: 500 * time.Millisecond, + MaxInterval: 60 * time.Second, + Exponent: 1.5, + MaxElapsedTime: 5 * time.Minute, + }, + RetryConnectionErrors: true, +}) +``` + +### WithOperationTimeout + +WithOperationTimeout allows setting the request timeout for a single request. + +```go +operations.WithOperationTimeout(30 * time.Second) +``` + +### WithSetHeaders + +WithSetHeaders allows setting custom headers on a per-request basis. If the request already contains headers matching the provided keys, they will be overwritten. + +```go +operations.WithSetHeaders(map[string]string{ + "X-Cache-TTL": "60", +}) +``` + +### WithURLOverride + +WithURLOverride allows overriding the default URL for an operation. + +```go +operations.WithURLOverride("/custom/path") +``` + +### WithAcceptHeaderOverride + +WithAcceptHeaderOverride allows overriding the `Accept` header for operations that support multiple response content types. + +```go +operations.WithAcceptHeaderOverride(operations.AcceptHeaderEnumApplicationJson) +``` \ No newline at end of file diff --git a/internal/sdk/environments.go b/internal/sdk/environments.go new file mode 100644 index 0000000..82cfe2b --- /dev/null +++ b/internal/sdk/environments.go @@ -0,0 +1,820 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/environments" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type Environments struct { + Internal *Internal + Files *EnvironmentsFiles + + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newEnvironments(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Environments { + return &Environments{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + Internal: newInternal(rootSDK, sdkConfig, hooks), + Files: newEnvironmentsFiles(rootSDK, sdkConfig, hooks), + } +} + +// ListEnvironments - Lists environments. +func (s *Environments) ListEnvironments(ctx context.Context, request *operations.ListEnvironmentsRequest, opts ...operations.Option) (*operations.ListEnvironmentsResponse, error) { + globals := operations.ListEnvironmentsGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/environments", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ListEnvironments", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ListEnvironmentsResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out environments.ListEnvironmentsResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ListEnvironmentsResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// CreateEnvironment - Creates an environment. +func (s *Environments) CreateEnvironment(ctx context.Context, request operations.CreateEnvironmentRequest, opts ...operations.Option) (*operations.CreateEnvironmentResponse, error) { + globals := operations.CreateEnvironmentGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/environments", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "CreateEnvironment", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CreateEnvironmentResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out environments.Environment + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Environment = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// DeleteEnvironment - Deletes an environment. +func (s *Environments) DeleteEnvironment(ctx context.Context, request operations.DeleteEnvironmentRequest, opts ...operations.Option) (*operations.DeleteEnvironmentResponse, error) { + globals := operations.DeleteEnvironmentGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/environments/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "DeleteEnvironment", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteEnvironmentResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Empty + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Empty = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// GetEnvironment - Gets an environment. +func (s *Environments) GetEnvironment(ctx context.Context, request operations.GetEnvironmentRequest, opts ...operations.Option) (*operations.GetEnvironmentResponse, error) { + globals := operations.GetEnvironmentGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/environments/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "GetEnvironment", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetEnvironmentResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out environments.Environment + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Environment = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} diff --git a/internal/sdk/environmentsfiles.go b/internal/sdk/environmentsfiles.go new file mode 100644 index 0000000..45168e2 --- /dev/null +++ b/internal/sdk/environmentsfiles.go @@ -0,0 +1,282 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/environments" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type EnvironmentsFiles struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newEnvironmentsFiles(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *EnvironmentsFiles { + return &EnvironmentsFiles{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// List - Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper. +func (s *EnvironmentsFiles) List(ctx context.Context, request operations.GetEnvironmentFilesRequest, opts ...operations.Option) (*operations.GetEnvironmentFilesResponse, error) { + globals := operations.GetEnvironmentFilesGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/environments/{environment}/files/{path}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "GetEnvironmentFiles", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetEnvironmentFilesResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out environments.GetEnvironmentFilesResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.GetEnvironmentFilesResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/files.go b/internal/sdk/files.go new file mode 100644 index 0000000..eb81a60 --- /dev/null +++ b/internal/sdk/files.go @@ -0,0 +1,890 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/spyzhov/ajson" +) + +// Files - Upload / list / download / delete media (48h TTL) +type Files struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newFiles(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Files { + return &Files{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// FilesList - Lists the metadata for `File`s owned by the requesting project. +func (s *Files) FilesList(ctx context.Context, request *operations.FilesListRequest, opts ...operations.Option) (*operations.FilesListResponse, error) { + globals := operations.FilesListGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/files", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "FilesList", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + paginationCtx := ctx + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.FilesListResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + res.Next = func() (*operations.FilesListResponse, error) { + if request == nil { + request = &operations.FilesListRequest{} + } + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return nil, err + } + nC, err := ajson.Eval(b, "$.nextPageToken") + if err != nil { + return nil, err + } + var nCVal string + + if nC.IsNumeric() { + numVal, err := nC.GetNumeric() + if err != nil { + return nil, err + } + // GetNumeric returns as float64 so convert to the appropriate type. + nCVal = strconv.FormatFloat(numVal, 'f', 0, 64) + } else { + val, err := nC.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + nCVal = val.(string) + if strings.TrimSpace(nCVal) == "" { + return nil, nil + } + } + request.PageToken = &nCVal + + return s.FilesList( + paginationCtx, + request, + opts..., + ) + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out genai.ListFilesResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ListFilesResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// FilesDelete - Deletes the `File`. +func (s *Files) FilesDelete(ctx context.Context, request operations.FilesDeleteRequest, opts ...operations.Option) (*operations.FilesDeleteResponse, error) { + globals := operations.FilesDeleteGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/files/{file}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "FilesDelete", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.FilesDeleteResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out genai.Empty + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Empty = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// FilesGet - Gets the metadata for the given `File`. +func (s *Files) FilesGet(ctx context.Context, request operations.FilesGetRequest, opts ...operations.Option) (*operations.FilesGetResponse, error) { + globals := operations.FilesGetGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/files/{file}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "FilesGet", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.FilesGetResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out genai.File + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.File = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// FilesRegister - Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails. +func (s *Files) FilesRegister(ctx context.Context, request operations.FilesRegisterRequest, opts ...operations.Option) (*operations.FilesRegisterResponse, error) { + globals := operations.FilesRegisterGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/files:register", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "FilesRegister", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.FilesRegisterResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out genai.RegisterFilesResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.RegisterFilesResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/geminiapi.go b/internal/sdk/geminiapi.go new file mode 100644 index 0000000..b0eeb45 --- /dev/null +++ b/internal/sdk/geminiapi.go @@ -0,0 +1,225 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +// Generated from OpenAPI doc version v1beta and generator version internal + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/globals" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ServerList contains the list of servers available to the SDK +var ServerList = []string{ + // Global Endpoint + "https://generativelanguage.googleapis.com", +} + +// HTTPClient provides an interface for supplying the SDK with a custom HTTP client +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// String provides a helper function to return a pointer to a string +func String(s string) *string { return &s } + +// Bool provides a helper function to return a pointer to a bool +func Bool(b bool) *bool { return &b } + +// Int provides a helper function to return a pointer to an int +func Int(i int) *int { return &i } + +// Int64 provides a helper function to return a pointer to an int64 +func Int64(i int64) *int64 { return &i } + +// Float32 provides a helper function to return a pointer to a float32 +func Float32(f float32) *float32 { return &f } + +// Float64 provides a helper function to return a pointer to a float64 +func Float64(f float64) *float64 { return &f } + +// Pointer provides a helper function to return a pointer to a type +func Pointer[T any](v T) *T { return &v } + +// GeminiAPI - Gemini API: Use the Gemini Interactions API and managed-agent platform from the command line. +// +// Get started: +// +// Set GEMINI_API_KEY, or run: gemini-api configure +// Then run a model or managed agent: gemini-api agent --help +// Add --dry-run to preview any API call without sending it. +type GeminiAPI struct { + SDKVersion string + Environments *Environments + // Run interactions with Gemini models or managed agents, and manage agent definitions. + // + // Start here: + // gemini-api agent run --help + // + // Other common flows: + // gemini-api agent create --help Define a managed agent + // gemini-api agent status --help Inspect a background interaction + // + // Note: agent IDs and interaction IDs are distinct resources. "agent status" + // takes an interaction ID; to inspect an agent definition use "agent get". + Agent *Agent + Credentials *Credentials + // Upload / list / download / delete media (48h TTL) + Files *Files + // Full model operations — list and get model metadata, embed, count tokens, and generate with complete request control + Models *Models + // Schedule and manage cron triggers that run managed agents + Triggers *Triggers + // Manage webhook endpoints and signing secrets for event delivery + Webhooks *Webhooks + + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +type SDKOption func(*GeminiAPI) + +// WithServerURL allows providing an alternative server URL +func WithServerURL(serverURL string) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.ServerURL = serverURL + } +} + +// WithTemplatedServerURL allows the overriding of the default server URL with a templated URL populated with the provided parameters +func WithTemplatedServerURL(serverURL string, params map[string]string) SDKOption { + return func(sdk *GeminiAPI) { + if params != nil { + serverURL = utils.ReplaceParameters(serverURL, params) + } + + sdk.sdkConfiguration.ServerURL = serverURL + } +} + +// WithServerIndex allows the overriding of the default server by index +func WithServerIndex(serverIndex int) SDKOption { + return func(sdk *GeminiAPI) { + if serverIndex < 0 || serverIndex >= len(ServerList) { + panic(fmt.Errorf("server index %d out of range", serverIndex)) + } + + sdk.sdkConfiguration.ServerIndex = serverIndex + } +} + +// WithClient allows the overriding of the default HTTP client used by the SDK +func WithClient(client HTTPClient) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Client = client + } +} + +// WithSecurity configures the SDK to use the provided security details +func WithSecurity(security components.Security) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Security = utils.AsSecuritySource(security) + } +} + +// WithSecuritySource configures the SDK to invoke the Security Source function on each method call to determine authentication +func WithSecuritySource(security func(context.Context) (components.Security, error)) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Security = func(ctx context.Context) (interface{}, error) { + return security(ctx) + } + } +} + +// WithAPIVersion allows setting the APIVersion parameter for all supported operations +func WithAPIVersion(apiVersion string) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Globals.APIVersion = &apiVersion + } +} + +// WithAPIRevision allows setting the APIRevision parameter for all supported operations +func WithAPIRevision(apiRevision string) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Globals.APIRevision = &apiRevision + } +} + +// WithUserProject allows setting the UserProject parameter for all supported operations +func WithUserProject(userProject string) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Globals.UserProject = &userProject + } +} + +func WithRetryConfig(retryConfig retry.Config) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.RetryConfig = &retryConfig + } +} + +// WithTimeout Optional request timeout applied to each operation +func WithTimeout(timeout time.Duration) SDKOption { + return func(sdk *GeminiAPI) { + sdk.sdkConfiguration.Timeout = &timeout + } +} + +// New creates a new instance of the SDK with the provided options +func New(opts ...SDKOption) *GeminiAPI { + sdk := &GeminiAPI{ + SDKVersion: "0.6.0", + sdkConfiguration: config.SDKConfiguration{ + UserAgent: "speakeasy-sdk/go 0.6.0 internal v1beta google3/third_party/gemini_api_cli/internal/sdk", + SDKVersion: "0.6.0", + GenVersion: "internal", + OpenAPIDocVersion: "v1beta", + Globals: globals.Globals{}, + ServerList: ServerList, + }, + hooks: hooks.New(), + } + for _, opt := range opts { + opt(sdk) + } + + // Use WithClient to override the default client if you would like to customize the timeout + if sdk.sdkConfiguration.Client == nil { + sdk.sdkConfiguration.Client = &http.Client{Timeout: 60 * time.Second} + } + + sdk.sdkConfiguration = sdk.hooks.SDKInit(sdk.sdkConfiguration) + + sdk.Environments = newEnvironments(sdk, sdk.sdkConfiguration, sdk.hooks) + sdk.Agent = newAgent(sdk, sdk.sdkConfiguration, sdk.hooks) + sdk.Credentials = newCredentials(sdk, sdk.sdkConfiguration, sdk.hooks) + sdk.Files = newFiles(sdk, sdk.sdkConfiguration, sdk.hooks) + sdk.Models = newModels(sdk, sdk.sdkConfiguration, sdk.hooks) + sdk.Triggers = newTriggers(sdk, sdk.sdkConfiguration, sdk.hooks) + sdk.Webhooks = newWebhooks(sdk, sdk.sdkConfiguration, sdk.hooks) + + return sdk +} diff --git a/internal/sdk/internal.go b/internal/sdk/internal.go new file mode 100644 index 0000000..8d5d461 --- /dev/null +++ b/internal/sdk/internal.go @@ -0,0 +1,185 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "context" + "fmt" + "net/http" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type Internal struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newInternal(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Internal { + return &Internal{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// StartUpload - Start an environment file upload +// Starts a resumable upload session for a file in an environment workspace. +// Upload the file bytes to the URL returned in the `X-Goog-Upload-URL` +// response header, using the resumable upload protocol. +func (s *Internal) StartUpload(ctx context.Context, request operations.StartEnvironmentFileUploadRequest, opts ...operations.Option) (*operations.StartEnvironmentFileUploadResponse, error) { + globals := operations.StartEnvironmentFileUploadGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/upload/{api_version}/environments/{environment}/files/{path}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "StartEnvironmentFileUpload", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "PUT", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "*/*") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.StartEnvironmentFileUploadResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + res.Headers = httpRes.Header + + if o.SkipDeserialization == nil || !*o.SkipDeserialization { + utils.DrainBody(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/models.go b/internal/sdk/models.go new file mode 100644 index 0000000..cca65f7 --- /dev/null +++ b/internal/sdk/models.go @@ -0,0 +1,565 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/spyzhov/ajson" +) + +// Models - Full model operations — list and get model metadata, embed, count tokens, and generate with complete request control +type Models struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newModels(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Models { + return &Models{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// ModelsList - Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API. +func (s *Models) ModelsList(ctx context.Context, request *operations.ModelsListRequest, opts ...operations.Option) (*operations.ModelsListResponse, error) { + globals := operations.ModelsListGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/models", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ModelsList", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + paginationCtx := ctx + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ModelsListResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + res.Next = func() (*operations.ModelsListResponse, error) { + if request == nil { + request = &operations.ModelsListRequest{} + } + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return nil, err + } + nC, err := ajson.Eval(b, "$.nextPageToken") + if err != nil { + return nil, err + } + var nCVal string + + if nC.IsNumeric() { + numVal, err := nC.GetNumeric() + if err != nil { + return nil, err + } + // GetNumeric returns as float64 so convert to the appropriate type. + nCVal = strconv.FormatFloat(numVal, 'f', 0, 64) + } else { + val, err := nC.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + nCVal = val.(string) + if strings.TrimSpace(nCVal) == "" { + return nil, nil + } + } + request.PageToken = &nCVal + + return s.ModelsList( + paginationCtx, + request, + opts..., + ) + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out genai.ListModelsResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ListModelsResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// ModelsGet - Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information. +func (s *Models) ModelsGet(ctx context.Context, request operations.ModelsGetRequest, opts ...operations.Option) (*operations.ModelsGetResponse, error) { + globals := operations.ModelsGetGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/models/{model}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ModelsGet", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ModelsGetResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out genai.Model + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Model = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/models/agents/agent.go b/internal/sdk/models/agents/agent.go new file mode 100644 index 0000000..74772c2 --- /dev/null +++ b/internal/sdk/models/agents/agent.go @@ -0,0 +1,321 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agents + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type AgentConfigType string + +const ( + AgentConfigTypeAntigravityAgentConfig AgentConfigType = "AntigravityAgentConfig" + AgentConfigTypeUnknown AgentConfigType = "Unknown" +) + +// AgentConfig - Configuration parameters for the agent. +type AgentConfig struct { + AntigravityAgentConfig *interactions.AntigravityAgentConfig `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type AgentConfigType +} + +func CreateAgentConfigAntigravityAgentConfig(antigravityAgentConfig interactions.AntigravityAgentConfig) AgentConfig { + typ := AgentConfigTypeAntigravityAgentConfig + + return AgentConfig{ + AntigravityAgentConfig: &antigravityAgentConfig, + Type: typ, + } +} + +func CreateAgentConfigUnknown(raw json.RawMessage) AgentConfig { + return AgentConfig{ + UnknownRaw: raw, + Type: AgentConfigTypeUnknown, + } +} + +func (u AgentConfig) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u AgentConfig) IsUnknown() bool { + return u.Type == AgentConfigTypeUnknown +} + +func (u *AgentConfig) UnmarshalJSON(data []byte) error { + *u = AgentConfig{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var antigravityAgentConfig interactions.AntigravityAgentConfig = interactions.AntigravityAgentConfig{} + if err := utils.UnmarshalJSON(data, &antigravityAgentConfig, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: AgentConfigTypeAntigravityAgentConfig, + Value: &antigravityAgentConfig, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = AgentConfigTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = AgentConfigTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(AgentConfigType) + switch best.Type { + case AgentConfigTypeAntigravityAgentConfig: + u.AntigravityAgentConfig = best.Value.(*interactions.AntigravityAgentConfig) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = AgentConfigTypeUnknown + return nil +} + +func (u AgentConfig) MarshalJSON() ([]byte, error) { + if u.AntigravityAgentConfig != nil { + return utils.MarshalJSON(u.AntigravityAgentConfig, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type AgentConfig: all fields are null") +} + +type BaseEnvironmentType string + +const ( + BaseEnvironmentTypeEnvironment BaseEnvironmentType = "Environment" + BaseEnvironmentTypeStr BaseEnvironmentType = "str" + BaseEnvironmentTypeUnknown BaseEnvironmentType = "Unknown" +) + +// BaseEnvironment - The environment configuration for the agent. +type BaseEnvironment struct { + Environment *interactions.Environment `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type BaseEnvironmentType +} + +func CreateBaseEnvironmentEnvironment(environment interactions.Environment) BaseEnvironment { + typ := BaseEnvironmentTypeEnvironment + + return BaseEnvironment{ + Environment: &environment, + Type: typ, + } +} + +func CreateBaseEnvironmentStr(str string) BaseEnvironment { + typ := BaseEnvironmentTypeStr + + return BaseEnvironment{ + Str: &str, + Type: typ, + } +} + +func CreateBaseEnvironmentUnknown(raw json.RawMessage) BaseEnvironment { + return BaseEnvironment{ + UnknownRaw: raw, + Type: BaseEnvironmentTypeUnknown, + } +} + +func (u BaseEnvironment) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u BaseEnvironment) IsUnknown() bool { + return u.Type == BaseEnvironmentTypeUnknown +} + +func (u *BaseEnvironment) UnmarshalJSON(data []byte) error { + *u = BaseEnvironment{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environment interactions.Environment = interactions.Environment{} + if err := utils.UnmarshalJSON(data, &environment, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: BaseEnvironmentTypeEnvironment, + Value: &environment, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: BaseEnvironmentTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = BaseEnvironmentTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = BaseEnvironmentTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(BaseEnvironmentType) + switch best.Type { + case BaseEnvironmentTypeEnvironment: + u.Environment = best.Value.(*interactions.Environment) + return nil + case BaseEnvironmentTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = BaseEnvironmentTypeUnknown + return nil +} + +func (u BaseEnvironment) MarshalJSON() ([]byte, error) { + if u.Environment != nil { + return utils.MarshalJSON(u.Environment, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type BaseEnvironment: all fields are null") +} + +// Agent - An agent definition for the CreateAgent API. +// This message is the target for annotation-parser-based JSON parsing. +// New format: +// +// { +// "id": "customer-sentinel", +// "base_agent": "", +// "system_instruction": "...", +// "base_environment": { "type": "remote", "sources": [...] }, +// "tools": [ {"type": "code_execution"} ] +// } +type Agent struct { + // Configuration parameters for the agent. + AgentConfig *AgentConfig `json:"agent_config,omitzero"` + // The base agent to extend. + BaseAgent string `json:"base_agent"` + // The environment configuration for the agent. + BaseEnvironment *BaseEnvironment `json:"base_environment,omitzero"` + // Agent description for developers to quickly read and understand. + Description *string `json:"description,omitzero"` + // The unique identifier for the agent. + ID string `json:"id"` + // System instruction for the agent. + SystemInstruction *string `json:"system_instruction,omitzero"` + // The tools available to the agent. + Tools []AgentTool `json:"tools,omitzero"` +} + +func (a Agent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *Agent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *Agent) GetAgentConfig() *AgentConfig { + if a == nil { + return nil + } + return a.AgentConfig +} + +func (a *Agent) GetBaseAgent() string { + if a == nil { + return "" + } + return a.BaseAgent +} + +func (a *Agent) GetBaseEnvironment() *BaseEnvironment { + if a == nil { + return nil + } + return a.BaseEnvironment +} + +func (a *Agent) GetDescription() *string { + if a == nil { + return nil + } + return a.Description +} + +func (a *Agent) GetID() string { + if a == nil { + return "" + } + return a.ID +} + +func (a *Agent) GetSystemInstruction() *string { + if a == nil { + return nil + } + return a.SystemInstruction +} + +func (a *Agent) GetTools() []AgentTool { + if a == nil { + return nil + } + return a.Tools +} diff --git a/internal/sdk/models/agents/agentlistresponse.go b/internal/sdk/models/agents/agentlistresponse.go new file mode 100644 index 0000000..64b4cd5 --- /dev/null +++ b/internal/sdk/models/agents/agentlistresponse.go @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agents + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type AgentListResponse struct { + // The list of agents. + Agents []Agent `json:"agents,omitzero"` + // A token to retrieve the next page of results. + NextPageToken *string `json:"next_page_token,omitzero"` +} + +func (a AgentListResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AgentListResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AgentListResponse) GetAgents() []Agent { + if a == nil { + return nil + } + return a.Agents +} + +func (a *AgentListResponse) GetNextPageToken() *string { + if a == nil { + return nil + } + return a.NextPageToken +} diff --git a/internal/sdk/models/agents/agenttool.go b/internal/sdk/models/agents/agenttool.go new file mode 100644 index 0000000..96b1533 --- /dev/null +++ b/internal/sdk/models/agents/agenttool.go @@ -0,0 +1,215 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package agents + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type AgentToolType string + +const ( + AgentToolTypeCodeExecution AgentToolType = "code_execution" + AgentToolTypeFunction AgentToolType = "function" + AgentToolTypeGoogleSearch AgentToolType = "google_search" + AgentToolTypeMcpServer AgentToolType = "mcp_server" + AgentToolTypeURLContext AgentToolType = "url_context" + AgentToolTypeUnknown AgentToolType = "UNKNOWN" +) + +// AgentTool - A tool that the agent can use. +type AgentTool struct { + CodeExecution *interactions.CodeExecution `queryParam:"inline" union:"member"` + Function *interactions.Function `queryParam:"inline" union:"member"` + GoogleSearch *interactions.GoogleSearch `queryParam:"inline" union:"member"` + MCPServer *interactions.MCPServer `queryParam:"inline" union:"member"` + URLContext *interactions.URLContext `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type AgentToolType +} + +func CreateAgentToolCodeExecution(codeExecution interactions.CodeExecution) AgentTool { + typ := AgentToolTypeCodeExecution + + return AgentTool{ + CodeExecution: &codeExecution, + Type: typ, + } +} + +func CreateAgentToolFunction(function interactions.Function) AgentTool { + typ := AgentToolTypeFunction + + return AgentTool{ + Function: &function, + Type: typ, + } +} + +func CreateAgentToolGoogleSearch(googleSearch interactions.GoogleSearch) AgentTool { + typ := AgentToolTypeGoogleSearch + + return AgentTool{ + GoogleSearch: &googleSearch, + Type: typ, + } +} + +func CreateAgentToolMcpServer(mcpServer interactions.MCPServer) AgentTool { + typ := AgentToolTypeMcpServer + + return AgentTool{ + MCPServer: &mcpServer, + Type: typ, + } +} + +func CreateAgentToolURLContext(urlContext interactions.URLContext) AgentTool { + typ := AgentToolTypeURLContext + + return AgentTool{ + URLContext: &urlContext, + Type: typ, + } +} + +func CreateAgentToolUnknown(raw json.RawMessage) AgentTool { + return AgentTool{ + UnknownRaw: raw, + Type: AgentToolTypeUnknown, + } +} + +func (u AgentTool) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u AgentTool) IsUnknown() bool { + return u.Type == AgentToolTypeUnknown +} + +func (u *AgentTool) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = AgentTool{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = AgentToolTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = AgentToolTypeUnknown + return nil + } + + switch dis.Type { + case "code_execution": + codeExecution := new(interactions.CodeExecution) + if err := utils.UnmarshalJSON(data, &codeExecution, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code_execution) type interactions.CodeExecution within AgentTool: %w", string(data), err) + } + + u.CodeExecution = codeExecution + u.Type = AgentToolTypeCodeExecution + return nil + case "function": + function := new(interactions.Function) + if err := utils.UnmarshalJSON(data, &function, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == function) type interactions.Function within AgentTool: %w", string(data), err) + } + + u.Function = function + u.Type = AgentToolTypeFunction + return nil + case "google_search": + googleSearch := new(interactions.GoogleSearch) + if err := utils.UnmarshalJSON(data, &googleSearch, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_search) type interactions.GoogleSearch within AgentTool: %w", string(data), err) + } + + u.GoogleSearch = googleSearch + u.Type = AgentToolTypeGoogleSearch + return nil + case "mcp_server": + mcpServer := new(interactions.MCPServer) + if err := utils.UnmarshalJSON(data, &mcpServer, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == mcp_server) type interactions.MCPServer within AgentTool: %w", string(data), err) + } + + u.MCPServer = mcpServer + u.Type = AgentToolTypeMcpServer + return nil + case "url_context": + urlContext := new(interactions.URLContext) + if err := utils.UnmarshalJSON(data, &urlContext, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_context) type interactions.URLContext within AgentTool: %w", string(data), err) + } + + u.URLContext = urlContext + u.Type = AgentToolTypeURLContext + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = AgentToolTypeUnknown + return nil + } + +} + +func (u AgentTool) MarshalJSON() ([]byte, error) { + if u.CodeExecution != nil { + return utils.MarshalJSON(u.CodeExecution, "", true) + } + + if u.Function != nil { + return utils.MarshalJSON(u.Function, "", true) + } + + if u.GoogleSearch != nil { + return utils.MarshalJSON(u.GoogleSearch, "", true) + } + + if u.MCPServer != nil { + return utils.MarshalJSON(u.MCPServer, "", true) + } + + if u.URLContext != nil { + return utils.MarshalJSON(u.URLContext, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type AgentTool: all fields are null") +} diff --git a/internal/sdk/models/components/httpmetadata.go b/internal/sdk/models/components/httpmetadata.go new file mode 100644 index 0000000..2391974 --- /dev/null +++ b/internal/sdk/models/components/httpmetadata.go @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +import ( + "net/http" +) + +type HTTPMetadata struct { + // Raw HTTP response; suitable for custom response parsing + Response *http.Response `json:"-"` + // Raw HTTP request; suitable for debugging + Request *http.Request `json:"-"` +} + +func (h *HTTPMetadata) GetResponse() *http.Response { + if h == nil { + return nil + } + return h.Response +} + +func (h *HTTPMetadata) GetRequest() *http.Request { + if h == nil { + return nil + } + return h.Request +} diff --git a/internal/sdk/models/components/security.go b/internal/sdk/models/components/security.go new file mode 100644 index 0000000..8cce216 --- /dev/null +++ b/internal/sdk/models/components/security.go @@ -0,0 +1,36 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type Security struct { + APIKey *string `security:"scheme,type=apiKey,subtype=header,name=x-goog-api-key"` + AccessToken *string `security:"scheme,type=http,subtype=bearer,name=Authorization"` +} + +func (s *Security) GetAPIKey() *string { + if s == nil { + return nil + } + return s.APIKey +} + +func (s *Security) GetAccessToken() *string { + if s == nil { + return nil + } + return s.AccessToken +} diff --git a/internal/sdk/models/credentials/credential.go b/internal/sdk/models/credentials/credential.go new file mode 100644 index 0000000..c888a61 --- /dev/null +++ b/internal/sdk/models/credentials/credential.go @@ -0,0 +1,130 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Status - Output only. Current status of the credential. +type Status string + +const ( + StatusActive Status = "active" + StatusRevoked Status = "revoked" +) + +func (e Status) ToPointer() *Status { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Status) IsExact() bool { + if e != nil { + switch *e { + case "active", "revoked": + return true + } + } + return false +} + +// Type - Required. Output only. The type of credential. +type Type string + +const ( + TypeBearerToken Type = "bearer_token" + TypeOauth2 Type = "oauth2" + TypeEnvironmentVariable Type = "environment_variable" +) + +func (e Type) ToPointer() *Type { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Type) IsExact() bool { + if e != nil { + switch *e { + case "bearer_token", "oauth2", "environment_variable": + return true + } + } + return false +} + +// Credential - Server-managed credential resource stored in Secret Manager. +type Credential struct { + // Output only. The timestamp when the credential was created. + CreateTime *time.Time `json:"create_time,omitzero"` + // Required. Output only. Identifier. Unique identifier for the credential. + ID string `json:"id"` + // Output only. Current status of the credential. + Status *Status `json:"status,omitzero"` + // Required. Output only. The type of credential. + Type *Type `json:"type,omitzero"` + // Output only. The timestamp when the credential was last updated. + UpdateTime *time.Time `json:"update_time,omitzero"` +} + +func (c Credential) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *Credential) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *Credential) GetCreateTime() *time.Time { + if c == nil { + return nil + } + return c.CreateTime +} + +func (c *Credential) GetID() string { + if c == nil { + return "" + } + return c.ID +} + +func (c *Credential) GetStatus() *Status { + if c == nil { + return nil + } + return c.Status +} + +func (c *Credential) GetType() *Type { + if c == nil { + return nil + } + return c.Type +} + +func (c *Credential) GetUpdateTime() *time.Time { + if c == nil { + return nil + } + return c.UpdateTime +} diff --git a/internal/sdk/models/credentials/credentialcreateparams.go b/internal/sdk/models/credentials/credentialcreateparams.go new file mode 100644 index 0000000..48d3a9e --- /dev/null +++ b/internal/sdk/models/credentials/credentialcreateparams.go @@ -0,0 +1,136 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CredentialCreateParamsType string + +const ( + CredentialCreateParamsTypeEnvironmentVariable CredentialCreateParamsType = "environment_variable" + CredentialCreateParamsTypeBearerToken CredentialCreateParamsType = "bearer_token" + CredentialCreateParamsTypeOauth2 CredentialCreateParamsType = "oauth2" +) + +// CredentialCreateParams - Represents the fields of a Credential provided on creation. +type CredentialCreateParams struct { + EnvironmentVariableConfig *EnvironmentVariableConfig `queryParam:"inline" union:"member"` + HTTPBearerConfig *HTTPBearerConfig `queryParam:"inline" union:"member"` + OAuth2Config *OAuth2Config `queryParam:"inline" union:"member"` + + Type CredentialCreateParamsType +} + +func CreateCredentialCreateParamsEnvironmentVariable(environmentVariable EnvironmentVariableConfig) CredentialCreateParams { + typ := CredentialCreateParamsTypeEnvironmentVariable + + return CredentialCreateParams{ + EnvironmentVariableConfig: &environmentVariable, + Type: typ, + } +} + +func CreateCredentialCreateParamsBearerToken(bearerToken HTTPBearerConfig) CredentialCreateParams { + typ := CredentialCreateParamsTypeBearerToken + + return CredentialCreateParams{ + HTTPBearerConfig: &bearerToken, + Type: typ, + } +} + +func CreateCredentialCreateParamsOauth2(oauth2 OAuth2Config) CredentialCreateParams { + typ := CredentialCreateParamsTypeOauth2 + + return CredentialCreateParams{ + OAuth2Config: &oauth2, + Type: typ, + } +} + +func (u *CredentialCreateParams) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CredentialCreateParams{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + return fmt.Errorf("could not unmarshal discriminator: %w", err) + } + + switch dis.Type { + case "environment_variable": + environmentVariableConfig := new(EnvironmentVariableConfig) + if err := utils.UnmarshalJSON(data, &environmentVariableConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == environment_variable) type EnvironmentVariableConfig within CredentialCreateParams: %w", string(data), err) + } + + u.EnvironmentVariableConfig = environmentVariableConfig + u.Type = CredentialCreateParamsTypeEnvironmentVariable + return nil + case "bearer_token": + httpBearerConfig := new(HTTPBearerConfig) + if err := utils.UnmarshalJSON(data, &httpBearerConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == bearer_token) type HTTPBearerConfig within CredentialCreateParams: %w", string(data), err) + } + + u.HTTPBearerConfig = httpBearerConfig + u.Type = CredentialCreateParamsTypeBearerToken + return nil + case "oauth2": + oAuth2Config := new(OAuth2Config) + if err := utils.UnmarshalJSON(data, &oAuth2Config, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == oauth2) type OAuth2Config within CredentialCreateParams: %w", string(data), err) + } + + u.OAuth2Config = oAuth2Config + u.Type = CredentialCreateParamsTypeOauth2 + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CredentialCreateParams", string(data)) +} + +func (u CredentialCreateParams) MarshalJSON() ([]byte, error) { + if u.EnvironmentVariableConfig != nil { + return utils.MarshalJSON(u.EnvironmentVariableConfig, "", true) + } + + if u.HTTPBearerConfig != nil { + return utils.MarshalJSON(u.HTTPBearerConfig, "", true) + } + + if u.OAuth2Config != nil { + return utils.MarshalJSON(u.OAuth2Config, "", true) + } + + return nil, errors.New("could not marshal union type CredentialCreateParams: all fields are null") +} diff --git a/internal/sdk/models/credentials/credentiallistresponse.go b/internal/sdk/models/credentials/credentiallistresponse.go new file mode 100644 index 0000000..b780b76 --- /dev/null +++ b/internal/sdk/models/credentials/credentiallistresponse.go @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CredentialListResponse struct { + Credentials []Credential `json:"credentials,omitzero"` + NextPageToken *string `json:"next_page_token,omitzero"` +} + +func (c CredentialListResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CredentialListResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CredentialListResponse) GetCredentials() []Credential { + if c == nil { + return nil + } + return c.Credentials +} + +func (c *CredentialListResponse) GetNextPageToken() *string { + if c == nil { + return nil + } + return c.NextPageToken +} diff --git a/internal/sdk/models/credentials/credentialupdate.go b/internal/sdk/models/credentials/credentialupdate.go new file mode 100644 index 0000000..50edf4d --- /dev/null +++ b/internal/sdk/models/credentials/credentialupdate.go @@ -0,0 +1,136 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CredentialUpdateType string + +const ( + CredentialUpdateTypeEnvironmentVariable CredentialUpdateType = "environment_variable" + CredentialUpdateTypeBearerToken CredentialUpdateType = "bearer_token" + CredentialUpdateTypeOauth2 CredentialUpdateType = "oauth2" +) + +// CredentialUpdate - Represents the fields of a Credential that can be updated. +type CredentialUpdate struct { + EnvironmentVariableUpdateConfig *EnvironmentVariableUpdateConfig `queryParam:"inline" union:"member"` + HTTPBearerUpdateConfig *HTTPBearerUpdateConfig `queryParam:"inline" union:"member"` + OAuth2UpdateConfig *OAuth2UpdateConfig `queryParam:"inline" union:"member"` + + Type CredentialUpdateType +} + +func CreateCredentialUpdateEnvironmentVariable(environmentVariable EnvironmentVariableUpdateConfig) CredentialUpdate { + typ := CredentialUpdateTypeEnvironmentVariable + + return CredentialUpdate{ + EnvironmentVariableUpdateConfig: &environmentVariable, + Type: typ, + } +} + +func CreateCredentialUpdateBearerToken(bearerToken HTTPBearerUpdateConfig) CredentialUpdate { + typ := CredentialUpdateTypeBearerToken + + return CredentialUpdate{ + HTTPBearerUpdateConfig: &bearerToken, + Type: typ, + } +} + +func CreateCredentialUpdateOauth2(oauth2 OAuth2UpdateConfig) CredentialUpdate { + typ := CredentialUpdateTypeOauth2 + + return CredentialUpdate{ + OAuth2UpdateConfig: &oauth2, + Type: typ, + } +} + +func (u *CredentialUpdate) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CredentialUpdate{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + return fmt.Errorf("could not unmarshal discriminator: %w", err) + } + + switch dis.Type { + case "environment_variable": + environmentVariableUpdateConfig := new(EnvironmentVariableUpdateConfig) + if err := utils.UnmarshalJSON(data, &environmentVariableUpdateConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == environment_variable) type EnvironmentVariableUpdateConfig within CredentialUpdate: %w", string(data), err) + } + + u.EnvironmentVariableUpdateConfig = environmentVariableUpdateConfig + u.Type = CredentialUpdateTypeEnvironmentVariable + return nil + case "bearer_token": + httpBearerUpdateConfig := new(HTTPBearerUpdateConfig) + if err := utils.UnmarshalJSON(data, &httpBearerUpdateConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == bearer_token) type HTTPBearerUpdateConfig within CredentialUpdate: %w", string(data), err) + } + + u.HTTPBearerUpdateConfig = httpBearerUpdateConfig + u.Type = CredentialUpdateTypeBearerToken + return nil + case "oauth2": + oAuth2UpdateConfig := new(OAuth2UpdateConfig) + if err := utils.UnmarshalJSON(data, &oAuth2UpdateConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == oauth2) type OAuth2UpdateConfig within CredentialUpdate: %w", string(data), err) + } + + u.OAuth2UpdateConfig = oAuth2UpdateConfig + u.Type = CredentialUpdateTypeOauth2 + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CredentialUpdate", string(data)) +} + +func (u CredentialUpdate) MarshalJSON() ([]byte, error) { + if u.EnvironmentVariableUpdateConfig != nil { + return utils.MarshalJSON(u.EnvironmentVariableUpdateConfig, "", true) + } + + if u.HTTPBearerUpdateConfig != nil { + return utils.MarshalJSON(u.HTTPBearerUpdateConfig, "", true) + } + + if u.OAuth2UpdateConfig != nil { + return utils.MarshalJSON(u.OAuth2UpdateConfig, "", true) + } + + return nil, errors.New("could not marshal union type CredentialUpdate: all fields are null") +} diff --git a/internal/sdk/models/credentials/environmentvariableconfig.go b/internal/sdk/models/credentials/environmentvariableconfig.go new file mode 100644 index 0000000..10a38e4 --- /dev/null +++ b/internal/sdk/models/credentials/environmentvariableconfig.go @@ -0,0 +1,183 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type EnvironmentVariableConfigInjectionLocationType string + +const ( + EnvironmentVariableConfigInjectionLocationTypeInjectionLocationEnum EnvironmentVariableConfigInjectionLocationType = "InjectionLocation_enum" + EnvironmentVariableConfigInjectionLocationTypeArrayOfInjectionLocationEnum EnvironmentVariableConfigInjectionLocationType = "arrayOfInjectionLocationEnum" +) + +// EnvironmentVariableConfigInjectionLocation - Required. Locations where the environment variable can be injected in +// outgoing HTTP requests. Must contain at least one location. +// Accepts either a single location (e.g. "header") or an array of locations. +type EnvironmentVariableConfigInjectionLocation struct { + InjectionLocationEnum *InjectionLocationEnum `queryParam:"inline" union:"member"` + ArrayOfInjectionLocationEnum []InjectionLocationEnum `queryParam:"inline" union:"member"` + + Type EnvironmentVariableConfigInjectionLocationType +} + +func CreateEnvironmentVariableConfigInjectionLocationInjectionLocationEnum(injectionLocationEnum InjectionLocationEnum) EnvironmentVariableConfigInjectionLocation { + typ := EnvironmentVariableConfigInjectionLocationTypeInjectionLocationEnum + + return EnvironmentVariableConfigInjectionLocation{ + InjectionLocationEnum: &injectionLocationEnum, + Type: typ, + } +} + +func CreateEnvironmentVariableConfigInjectionLocationArrayOfInjectionLocationEnum(arrayOfInjectionLocationEnum []InjectionLocationEnum) EnvironmentVariableConfigInjectionLocation { + typ := EnvironmentVariableConfigInjectionLocationTypeArrayOfInjectionLocationEnum + + return EnvironmentVariableConfigInjectionLocation{ + ArrayOfInjectionLocationEnum: arrayOfInjectionLocationEnum, + Type: typ, + } +} + +func (u *EnvironmentVariableConfigInjectionLocation) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = EnvironmentVariableConfigInjectionLocation{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var injectionLocationEnum InjectionLocationEnum = InjectionLocationEnum("") + if err := utils.UnmarshalJSON(data, &injectionLocationEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentVariableConfigInjectionLocationTypeInjectionLocationEnum, + Value: &injectionLocationEnum, + }) + } + + var arrayOfInjectionLocationEnum []InjectionLocationEnum = []InjectionLocationEnum{} + if err := utils.UnmarshalJSON(data, &arrayOfInjectionLocationEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentVariableConfigInjectionLocationTypeArrayOfInjectionLocationEnum, + Value: arrayOfInjectionLocationEnum, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for EnvironmentVariableConfigInjectionLocation", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for EnvironmentVariableConfigInjectionLocation", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(EnvironmentVariableConfigInjectionLocationType) + switch best.Type { + case EnvironmentVariableConfigInjectionLocationTypeInjectionLocationEnum: + u.InjectionLocationEnum = best.Value.(*InjectionLocationEnum) + return nil + case EnvironmentVariableConfigInjectionLocationTypeArrayOfInjectionLocationEnum: + u.ArrayOfInjectionLocationEnum = best.Value.([]InjectionLocationEnum) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for EnvironmentVariableConfigInjectionLocation", string(data)) +} + +func (u EnvironmentVariableConfigInjectionLocation) MarshalJSON() ([]byte, error) { + if u.InjectionLocationEnum != nil { + return utils.MarshalJSON(u.InjectionLocationEnum, "", true) + } + + if u.ArrayOfInjectionLocationEnum != nil { + return utils.MarshalJSON(u.ArrayOfInjectionLocationEnum, "", true) + } + + return nil, errors.New("could not marshal union type EnvironmentVariableConfigInjectionLocation: all fields are null") +} + +// EnvironmentVariableConfig - Configuration for environment variable credentials. +type EnvironmentVariableConfig struct { + ID string `json:"id"` + // Required. Locations where the environment variable can be injected in + // outgoing HTTP requests. Must contain at least one location. + // Accepts either a single location (e.g. "header") or an array of locations. + InjectionLocation EnvironmentVariableConfigInjectionLocation `json:"injection_location"` + // Optional. List of domains allowed to receive this environment variable + // value in HTTP requests. + TrustedDomains []string `json:"trusted_domains,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"environment_variable" json:"type"` + // Required. Input only. Secret value of the environment variable. Write-only; never + // returned in responses. + Value string `json:"value"` +} + +func (e EnvironmentVariableConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *EnvironmentVariableConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *EnvironmentVariableConfig) GetID() string { + if e == nil { + return "" + } + return e.ID +} + +func (e *EnvironmentVariableConfig) GetInjectionLocation() EnvironmentVariableConfigInjectionLocation { + if e == nil { + return EnvironmentVariableConfigInjectionLocation{} + } + return e.InjectionLocation +} + +func (e *EnvironmentVariableConfig) GetTrustedDomains() []string { + if e == nil { + return nil + } + return e.TrustedDomains +} + +func (e *EnvironmentVariableConfig) GetType() string { + return "environment_variable" +} + +func (e *EnvironmentVariableConfig) GetValue() string { + if e == nil { + return "" + } + return e.Value +} diff --git a/internal/sdk/models/credentials/environmentvariableupdateconfig.go b/internal/sdk/models/credentials/environmentvariableupdateconfig.go new file mode 100644 index 0000000..ff11880 --- /dev/null +++ b/internal/sdk/models/credentials/environmentvariableupdateconfig.go @@ -0,0 +1,175 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type EnvironmentVariableUpdateConfigInjectionLocationType string + +const ( + EnvironmentVariableUpdateConfigInjectionLocationTypeInjectionLocationEnum EnvironmentVariableUpdateConfigInjectionLocationType = "InjectionLocation_enum" + EnvironmentVariableUpdateConfigInjectionLocationTypeArrayOfInjectionLocationEnum EnvironmentVariableUpdateConfigInjectionLocationType = "arrayOfInjectionLocationEnum" +) + +// EnvironmentVariableUpdateConfigInjectionLocation - Optional. Locations where the environment variable can be injected in +// outgoing HTTP requests. +// Accepts either a single location (e.g. "header") or an array of locations. +type EnvironmentVariableUpdateConfigInjectionLocation struct { + InjectionLocationEnum *InjectionLocationEnum `queryParam:"inline" union:"member"` + ArrayOfInjectionLocationEnum []InjectionLocationEnum `queryParam:"inline" union:"member"` + + Type EnvironmentVariableUpdateConfigInjectionLocationType +} + +func CreateEnvironmentVariableUpdateConfigInjectionLocationInjectionLocationEnum(injectionLocationEnum InjectionLocationEnum) EnvironmentVariableUpdateConfigInjectionLocation { + typ := EnvironmentVariableUpdateConfigInjectionLocationTypeInjectionLocationEnum + + return EnvironmentVariableUpdateConfigInjectionLocation{ + InjectionLocationEnum: &injectionLocationEnum, + Type: typ, + } +} + +func CreateEnvironmentVariableUpdateConfigInjectionLocationArrayOfInjectionLocationEnum(arrayOfInjectionLocationEnum []InjectionLocationEnum) EnvironmentVariableUpdateConfigInjectionLocation { + typ := EnvironmentVariableUpdateConfigInjectionLocationTypeArrayOfInjectionLocationEnum + + return EnvironmentVariableUpdateConfigInjectionLocation{ + ArrayOfInjectionLocationEnum: arrayOfInjectionLocationEnum, + Type: typ, + } +} + +func (u *EnvironmentVariableUpdateConfigInjectionLocation) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = EnvironmentVariableUpdateConfigInjectionLocation{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var injectionLocationEnum InjectionLocationEnum = InjectionLocationEnum("") + if err := utils.UnmarshalJSON(data, &injectionLocationEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentVariableUpdateConfigInjectionLocationTypeInjectionLocationEnum, + Value: &injectionLocationEnum, + }) + } + + var arrayOfInjectionLocationEnum []InjectionLocationEnum = []InjectionLocationEnum{} + if err := utils.UnmarshalJSON(data, &arrayOfInjectionLocationEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentVariableUpdateConfigInjectionLocationTypeArrayOfInjectionLocationEnum, + Value: arrayOfInjectionLocationEnum, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for EnvironmentVariableUpdateConfigInjectionLocation", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for EnvironmentVariableUpdateConfigInjectionLocation", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(EnvironmentVariableUpdateConfigInjectionLocationType) + switch best.Type { + case EnvironmentVariableUpdateConfigInjectionLocationTypeInjectionLocationEnum: + u.InjectionLocationEnum = best.Value.(*InjectionLocationEnum) + return nil + case EnvironmentVariableUpdateConfigInjectionLocationTypeArrayOfInjectionLocationEnum: + u.ArrayOfInjectionLocationEnum = best.Value.([]InjectionLocationEnum) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for EnvironmentVariableUpdateConfigInjectionLocation", string(data)) +} + +func (u EnvironmentVariableUpdateConfigInjectionLocation) MarshalJSON() ([]byte, error) { + if u.InjectionLocationEnum != nil { + return utils.MarshalJSON(u.InjectionLocationEnum, "", true) + } + + if u.ArrayOfInjectionLocationEnum != nil { + return utils.MarshalJSON(u.ArrayOfInjectionLocationEnum, "", true) + } + + return nil, errors.New("could not marshal union type EnvironmentVariableUpdateConfigInjectionLocation: all fields are null") +} + +// EnvironmentVariableUpdateConfig - Configuration for updating environment variable credentials. +type EnvironmentVariableUpdateConfig struct { + // Optional. Locations where the environment variable can be injected in + // outgoing HTTP requests. + // Accepts either a single location (e.g. "header") or an array of locations. + InjectionLocation *EnvironmentVariableUpdateConfigInjectionLocation `json:"injection_location,omitzero"` + // Optional. List of domains allowed to receive this environment variable + // value in HTTP requests. + TrustedDomains []string `json:"trusted_domains,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"environment_variable" json:"type"` + // Optional. Input only. Secret value of the environment variable. Write-only; never + // returned in responses. + Value *string `json:"value,omitzero"` +} + +func (e EnvironmentVariableUpdateConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *EnvironmentVariableUpdateConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *EnvironmentVariableUpdateConfig) GetInjectionLocation() *EnvironmentVariableUpdateConfigInjectionLocation { + if e == nil { + return nil + } + return e.InjectionLocation +} + +func (e *EnvironmentVariableUpdateConfig) GetTrustedDomains() []string { + if e == nil { + return nil + } + return e.TrustedDomains +} + +func (e *EnvironmentVariableUpdateConfig) GetType() string { + return "environment_variable" +} + +func (e *EnvironmentVariableUpdateConfig) GetValue() *string { + if e == nil { + return nil + } + return e.Value +} diff --git a/internal/sdk/models/credentials/httpbearerconfig.go b/internal/sdk/models/credentials/httpbearerconfig.go new file mode 100644 index 0000000..31cd1e6 --- /dev/null +++ b/internal/sdk/models/credentials/httpbearerconfig.go @@ -0,0 +1,79 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// HTTPBearerConfig - Configuration for HTTP Bearer token credentials. +type HTTPBearerConfig struct { + // Optional. Header name to inject the token into. Defaults to + // 'Authorization'. + HeaderName *string `json:"header_name,omitzero"` + ID string `json:"id"` + // Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to '' + // for no prefix. + Prefix *string `json:"prefix,omitzero"` + // Required. Input only. The static bearer token. Write-only; never returned in responses. + Token string `json:"token"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"bearer_token" json:"type"` +} + +func (h HTTPBearerConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(h, "", false) +} + +func (h *HTTPBearerConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &h, "", false, nil); err != nil { + return err + } + return nil +} + +func (h *HTTPBearerConfig) GetHeaderName() *string { + if h == nil { + return nil + } + return h.HeaderName +} + +func (h *HTTPBearerConfig) GetID() string { + if h == nil { + return "" + } + return h.ID +} + +func (h *HTTPBearerConfig) GetPrefix() *string { + if h == nil { + return nil + } + return h.Prefix +} + +func (h *HTTPBearerConfig) GetToken() string { + if h == nil { + return "" + } + return h.Token +} + +func (h *HTTPBearerConfig) GetType() string { + return "bearer_token" +} diff --git a/internal/sdk/models/credentials/httpbearerupdateconfig.go b/internal/sdk/models/credentials/httpbearerupdateconfig.go new file mode 100644 index 0000000..b5a478c --- /dev/null +++ b/internal/sdk/models/credentials/httpbearerupdateconfig.go @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// HTTPBearerUpdateConfig - Configuration for updating HTTP Bearer token credentials. +type HTTPBearerUpdateConfig struct { + // Optional. Header name to inject the token into. Defaults to + // 'Authorization'. + HeaderName *string `json:"header_name,omitzero"` + // Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to '' + // for no prefix. + Prefix *string `json:"prefix,omitzero"` + // Optional. Input only. The static bearer token. Write-only; never returned in responses. + Token *string `json:"token,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"bearer_token" json:"type"` +} + +func (h HTTPBearerUpdateConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(h, "", false) +} + +func (h *HTTPBearerUpdateConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &h, "", false, nil); err != nil { + return err + } + return nil +} + +func (h *HTTPBearerUpdateConfig) GetHeaderName() *string { + if h == nil { + return nil + } + return h.HeaderName +} + +func (h *HTTPBearerUpdateConfig) GetPrefix() *string { + if h == nil { + return nil + } + return h.Prefix +} + +func (h *HTTPBearerUpdateConfig) GetToken() *string { + if h == nil { + return nil + } + return h.Token +} + +func (h *HTTPBearerUpdateConfig) GetType() string { + return "bearer_token" +} diff --git a/internal/sdk/models/credentials/injectionlocationenum.go b/internal/sdk/models/credentials/injectionlocationenum.go new file mode 100644 index 0000000..c961138 --- /dev/null +++ b/internal/sdk/models/credentials/injectionlocationenum.go @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "encoding/json" + "fmt" +) + +type InjectionLocationEnum string + +const ( + InjectionLocationEnumHeader InjectionLocationEnum = "header" + InjectionLocationEnumQuery InjectionLocationEnum = "query" + InjectionLocationEnumBody InjectionLocationEnum = "body" +) + +func (e InjectionLocationEnum) ToPointer() *InjectionLocationEnum { + return &e +} +func (e *InjectionLocationEnum) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "header": + fallthrough + case "query": + fallthrough + case "body": + *e = InjectionLocationEnum(v) + return nil + default: + return fmt.Errorf("invalid value for InjectionLocationEnum: %v", v) + } +} diff --git a/internal/sdk/models/credentials/oauth2config.go b/internal/sdk/models/credentials/oauth2config.go new file mode 100644 index 0000000..07e0e6a --- /dev/null +++ b/internal/sdk/models/credentials/oauth2config.go @@ -0,0 +1,98 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// OAuth2Config - Configuration for OAuth2 credentials with automatic token refresh. +type OAuth2Config struct { + // Required. OAuth2 client ID. + ClientID string `json:"client_id"` + // Required. Input only. OAuth2 client secret. Write-only; never returned in responses. + ClientSecret string `json:"client_secret"` + ID string `json:"id"` + // Required. Input only. OAuth2 refresh token. Write-only; never returned in responses. + RefreshToken string `json:"refresh_token"` + // Optional. List of OAuth2 scopes. + Scopes []string `json:"scopes,omitzero"` + // Required. OAuth2 token endpoint URL for refreshing access tokens. + TokenURL string `json:"token_url"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"oauth2" json:"type"` +} + +func (o OAuth2Config) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(o, "", false) +} + +func (o *OAuth2Config) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &o, "", false, nil); err != nil { + return err + } + return nil +} + +func (o *OAuth2Config) GetClientID() string { + if o == nil { + return "" + } + return o.ClientID +} + +func (o *OAuth2Config) GetClientSecret() string { + if o == nil { + return "" + } + return o.ClientSecret +} + +func (o *OAuth2Config) GetID() string { + if o == nil { + return "" + } + return o.ID +} + +func (o *OAuth2Config) GetRefreshToken() string { + if o == nil { + return "" + } + return o.RefreshToken +} + +func (o *OAuth2Config) GetScopes() []string { + if o == nil { + return nil + } + return o.Scopes +} + +func (o *OAuth2Config) GetTokenURL() string { + if o == nil { + return "" + } + return o.TokenURL +} + +func (o *OAuth2Config) GetType() string { + return "oauth2" +} + +// #region class-body-oauth2config +// #endregion class-body-oauth2config diff --git a/internal/sdk/models/credentials/oauth2updateconfig.go b/internal/sdk/models/credentials/oauth2updateconfig.go new file mode 100644 index 0000000..c0ba7b2 --- /dev/null +++ b/internal/sdk/models/credentials/oauth2updateconfig.go @@ -0,0 +1,90 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package credentials + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// OAuth2UpdateConfig - Configuration for updating OAuth2 credentials. +type OAuth2UpdateConfig struct { + // Optional. OAuth2 client ID. + ClientID *string `json:"client_id,omitzero"` + // Optional. Input only. OAuth2 client secret. Write-only; never returned in responses. + ClientSecret *string `json:"client_secret,omitzero"` + // Optional. Input only. OAuth2 refresh token. Write-only; never returned in responses. + RefreshToken *string `json:"refresh_token,omitzero"` + // Optional. List of OAuth2 scopes. + Scopes []string `json:"scopes,omitzero"` + // Optional. OAuth2 token endpoint URL for refreshing access tokens. + TokenURL *string `json:"token_url,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"oauth2" json:"type"` +} + +func (o OAuth2UpdateConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(o, "", false) +} + +func (o *OAuth2UpdateConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &o, "", false, nil); err != nil { + return err + } + return nil +} + +func (o *OAuth2UpdateConfig) GetClientID() *string { + if o == nil { + return nil + } + return o.ClientID +} + +func (o *OAuth2UpdateConfig) GetClientSecret() *string { + if o == nil { + return nil + } + return o.ClientSecret +} + +func (o *OAuth2UpdateConfig) GetRefreshToken() *string { + if o == nil { + return nil + } + return o.RefreshToken +} + +func (o *OAuth2UpdateConfig) GetScopes() []string { + if o == nil { + return nil + } + return o.Scopes +} + +func (o *OAuth2UpdateConfig) GetTokenURL() *string { + if o == nil { + return nil + } + return o.TokenURL +} + +func (o *OAuth2UpdateConfig) GetType() string { + return "oauth2" +} + +// #region class-body-oauth2updateconfig +// #endregion class-body-oauth2updateconfig diff --git a/internal/sdk/models/environments/createenvironmentrequest.go b/internal/sdk/models/environments/createenvironmentrequest.go new file mode 100644 index 0000000..b9def41 --- /dev/null +++ b/internal/sdk/models/environments/createenvironmentrequest.go @@ -0,0 +1,190 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateEnvironmentRequestNetworkEnum string + +const ( + CreateEnvironmentRequestNetworkEnumDisabled CreateEnvironmentRequestNetworkEnum = "disabled" +) + +func (e CreateEnvironmentRequestNetworkEnum) ToPointer() *CreateEnvironmentRequestNetworkEnum { + return &e +} +func (e *CreateEnvironmentRequestNetworkEnum) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "disabled": + *e = CreateEnvironmentRequestNetworkEnum(v) + return nil + default: + return fmt.Errorf("invalid value for CreateEnvironmentRequestNetworkEnum: %v", v) + } +} + +type CreateEnvironmentRequestNetworkUnionType string + +const ( + CreateEnvironmentRequestNetworkUnionTypeEnvironmentNetworkEgressAllowlist CreateEnvironmentRequestNetworkUnionType = "EnvironmentNetworkEgressAllowlist" + CreateEnvironmentRequestNetworkUnionTypeCreateEnvironmentRequestNetworkEnum CreateEnvironmentRequestNetworkUnionType = "CreateEnvironmentRequest_network_enum" +) + +// CreateEnvironmentRequestNetworkUnion - Network configuration for the environment. +type CreateEnvironmentRequestNetworkUnion struct { + EnvironmentNetworkEgressAllowlist *interactions.EnvironmentNetworkEgressAllowlist `queryParam:"inline" union:"member"` + CreateEnvironmentRequestNetworkEnum *CreateEnvironmentRequestNetworkEnum `queryParam:"inline" union:"member"` + + Type CreateEnvironmentRequestNetworkUnionType +} + +func CreateCreateEnvironmentRequestNetworkUnionEnvironmentNetworkEgressAllowlist(environmentNetworkEgressAllowlist interactions.EnvironmentNetworkEgressAllowlist) CreateEnvironmentRequestNetworkUnion { + typ := CreateEnvironmentRequestNetworkUnionTypeEnvironmentNetworkEgressAllowlist + + return CreateEnvironmentRequestNetworkUnion{ + EnvironmentNetworkEgressAllowlist: &environmentNetworkEgressAllowlist, + Type: typ, + } +} + +func CreateCreateEnvironmentRequestNetworkUnionCreateEnvironmentRequestNetworkEnum(createEnvironmentRequestNetworkEnum CreateEnvironmentRequestNetworkEnum) CreateEnvironmentRequestNetworkUnion { + typ := CreateEnvironmentRequestNetworkUnionTypeCreateEnvironmentRequestNetworkEnum + + return CreateEnvironmentRequestNetworkUnion{ + CreateEnvironmentRequestNetworkEnum: &createEnvironmentRequestNetworkEnum, + Type: typ, + } +} + +func (u *CreateEnvironmentRequestNetworkUnion) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateEnvironmentRequestNetworkUnion{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environmentNetworkEgressAllowlist interactions.EnvironmentNetworkEgressAllowlist = interactions.EnvironmentNetworkEgressAllowlist{} + if err := utils.UnmarshalJSON(data, &environmentNetworkEgressAllowlist, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateEnvironmentRequestNetworkUnionTypeEnvironmentNetworkEgressAllowlist, + Value: &environmentNetworkEgressAllowlist, + }) + } + + var createEnvironmentRequestNetworkEnum CreateEnvironmentRequestNetworkEnum = CreateEnvironmentRequestNetworkEnum("") + if err := utils.UnmarshalJSON(data, &createEnvironmentRequestNetworkEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateEnvironmentRequestNetworkUnionTypeCreateEnvironmentRequestNetworkEnum, + Value: &createEnvironmentRequestNetworkEnum, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateEnvironmentRequestNetworkUnion", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateEnvironmentRequestNetworkUnion", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(CreateEnvironmentRequestNetworkUnionType) + switch best.Type { + case CreateEnvironmentRequestNetworkUnionTypeEnvironmentNetworkEgressAllowlist: + u.EnvironmentNetworkEgressAllowlist = best.Value.(*interactions.EnvironmentNetworkEgressAllowlist) + return nil + case CreateEnvironmentRequestNetworkUnionTypeCreateEnvironmentRequestNetworkEnum: + u.CreateEnvironmentRequestNetworkEnum = best.Value.(*CreateEnvironmentRequestNetworkEnum) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateEnvironmentRequestNetworkUnion", string(data)) +} + +func (u CreateEnvironmentRequestNetworkUnion) MarshalJSON() ([]byte, error) { + if u.EnvironmentNetworkEgressAllowlist != nil { + return utils.MarshalJSON(u.EnvironmentNetworkEgressAllowlist, "", true) + } + + if u.CreateEnvironmentRequestNetworkEnum != nil { + return utils.MarshalJSON(u.CreateEnvironmentRequestNetworkEnum, "", true) + } + + return nil, errors.New("could not marshal union type CreateEnvironmentRequestNetworkUnion: all fields are null") +} + +// CreateEnvironmentRequest - Request for `CreateEnvironment`. +type CreateEnvironmentRequest struct { + // Optional. The source environment to copy/fork from. + // Format: `environments/{environment_id}` or `{environment_id}`. + // When specified, `sources` and `env` must be empty. + FromEnvironment *string `json:"from_environment,omitzero"` + // Network configuration for the environment. + Network *CreateEnvironmentRequestNetworkUnion `json:"network,omitzero"` + // Sources to be mounted into the environment. + Sources []interactions.Source `json:"sources,omitzero"` +} + +func (c CreateEnvironmentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateEnvironmentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateEnvironmentRequest) GetFromEnvironment() *string { + if c == nil { + return nil + } + return c.FromEnvironment +} + +func (c *CreateEnvironmentRequest) GetNetwork() *CreateEnvironmentRequestNetworkUnion { + if c == nil { + return nil + } + return c.Network +} + +func (c *CreateEnvironmentRequest) GetSources() []interactions.Source { + if c == nil { + return nil + } + return c.Sources +} diff --git a/internal/sdk/models/environments/environment.go b/internal/sdk/models/environments/environment.go new file mode 100644 index 0000000..1562062 --- /dev/null +++ b/internal/sdk/models/environments/environment.go @@ -0,0 +1,288 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type EnvironmentNetworkEnum string + +const ( + EnvironmentNetworkEnumDisabled EnvironmentNetworkEnum = "disabled" +) + +func (e EnvironmentNetworkEnum) ToPointer() *EnvironmentNetworkEnum { + return &e +} +func (e *EnvironmentNetworkEnum) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "disabled": + *e = EnvironmentNetworkEnum(v) + return nil + default: + return fmt.Errorf("invalid value for EnvironmentNetworkEnum: %v", v) + } +} + +type EnvironmentNetworkUnionType string + +const ( + EnvironmentNetworkUnionTypeEnvironmentNetworkEgressAllowlist EnvironmentNetworkUnionType = "EnvironmentNetworkEgressAllowlist" + EnvironmentNetworkUnionTypeEnvironmentNetworkEnum EnvironmentNetworkUnionType = "Environment_network_enum" + EnvironmentNetworkUnionTypeUnknown EnvironmentNetworkUnionType = "Unknown" +) + +// EnvironmentNetworkUnion - Network configuration for the environment. +type EnvironmentNetworkUnion struct { + EnvironmentNetworkEgressAllowlist *interactions.EnvironmentNetworkEgressAllowlist `queryParam:"inline" union:"member"` + EnvironmentNetworkEnum *EnvironmentNetworkEnum `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type EnvironmentNetworkUnionType +} + +func CreateEnvironmentNetworkUnionEnvironmentNetworkEgressAllowlist(environmentNetworkEgressAllowlist interactions.EnvironmentNetworkEgressAllowlist) EnvironmentNetworkUnion { + typ := EnvironmentNetworkUnionTypeEnvironmentNetworkEgressAllowlist + + return EnvironmentNetworkUnion{ + EnvironmentNetworkEgressAllowlist: &environmentNetworkEgressAllowlist, + Type: typ, + } +} + +func CreateEnvironmentNetworkUnionEnvironmentNetworkEnum(environmentNetworkEnum EnvironmentNetworkEnum) EnvironmentNetworkUnion { + typ := EnvironmentNetworkUnionTypeEnvironmentNetworkEnum + + return EnvironmentNetworkUnion{ + EnvironmentNetworkEnum: &environmentNetworkEnum, + Type: typ, + } +} + +func CreateEnvironmentNetworkUnionUnknown(raw json.RawMessage) EnvironmentNetworkUnion { + return EnvironmentNetworkUnion{ + UnknownRaw: raw, + Type: EnvironmentNetworkUnionTypeUnknown, + } +} + +func (u EnvironmentNetworkUnion) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u EnvironmentNetworkUnion) IsUnknown() bool { + return u.Type == EnvironmentNetworkUnionTypeUnknown +} + +func (u *EnvironmentNetworkUnion) UnmarshalJSON(data []byte) error { + *u = EnvironmentNetworkUnion{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environmentNetworkEgressAllowlist interactions.EnvironmentNetworkEgressAllowlist = interactions.EnvironmentNetworkEgressAllowlist{} + if err := utils.UnmarshalJSON(data, &environmentNetworkEgressAllowlist, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentNetworkUnionTypeEnvironmentNetworkEgressAllowlist, + Value: &environmentNetworkEgressAllowlist, + }) + } + + var environmentNetworkEnum EnvironmentNetworkEnum = EnvironmentNetworkEnum("") + if err := utils.UnmarshalJSON(data, &environmentNetworkEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentNetworkUnionTypeEnvironmentNetworkEnum, + Value: &environmentNetworkEnum, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvironmentNetworkUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvironmentNetworkUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(EnvironmentNetworkUnionType) + switch best.Type { + case EnvironmentNetworkUnionTypeEnvironmentNetworkEgressAllowlist: + u.EnvironmentNetworkEgressAllowlist = best.Value.(*interactions.EnvironmentNetworkEgressAllowlist) + return nil + case EnvironmentNetworkUnionTypeEnvironmentNetworkEnum: + u.EnvironmentNetworkEnum = best.Value.(*EnvironmentNetworkEnum) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvironmentNetworkUnionTypeUnknown + return nil +} + +func (u EnvironmentNetworkUnion) MarshalJSON() ([]byte, error) { + if u.EnvironmentNetworkEgressAllowlist != nil { + return utils.MarshalJSON(u.EnvironmentNetworkEgressAllowlist, "", true) + } + + if u.EnvironmentNetworkEnum != nil { + return utils.MarshalJSON(u.EnvironmentNetworkEnum, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type EnvironmentNetworkUnion: all fields are null") +} + +// Status - Output only. The status of the environment container. +type Status string + +const ( + StatusActive Status = "active" + StatusExpired Status = "expired" +) + +func (e Status) ToPointer() *Status { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Status) IsExact() bool { + if e != nil { + switch *e { + case "active", "expired": + return true + } + } + return false +} + +// Environment - An execution environment for an agent. +type Environment struct { + // Output only. The time at which the environment was created in ISO 8601 format + // (YYYY-MM-DDThh:mm:ssZ). + Created *string `json:"created,omitzero"` + // Output only. The number of files in the environment, output only. + FileCount *string `json:"file_count,omitzero"` + // Required. Output only. The ID of the environment. + ID string `json:"id"` + // Output only. The time at which the environment was last accessed in ISO 8601 format + // (YYYY-MM-DDThh:mm:ssZ). + LastAccessed *string `json:"last_accessed,omitzero"` + // Network configuration for the environment. + Network *EnvironmentNetworkUnion `json:"network,omitzero"` + // Output only. The total size of the environment files in bytes, output only. + SizeBytes *string `json:"size_bytes,omitzero"` + // Sources to be mounted into the environment. + Sources []interactions.Source `json:"sources,omitzero"` + // Output only. The status of the environment container. + Status *Status `json:"status,omitzero"` + // Output only. The time at which the environment was last updated in ISO 8601 format + // (YYYY-MM-DDThh:mm:ssZ). + Updated *string `json:"updated,omitzero"` +} + +func (e Environment) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *Environment) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *Environment) GetCreated() *string { + if e == nil { + return nil + } + return e.Created +} + +func (e *Environment) GetFileCount() *string { + if e == nil { + return nil + } + return e.FileCount +} + +func (e *Environment) GetID() string { + if e == nil { + return "" + } + return e.ID +} + +func (e *Environment) GetLastAccessed() *string { + if e == nil { + return nil + } + return e.LastAccessed +} + +func (e *Environment) GetNetwork() *EnvironmentNetworkUnion { + if e == nil { + return nil + } + return e.Network +} + +func (e *Environment) GetSizeBytes() *string { + if e == nil { + return nil + } + return e.SizeBytes +} + +func (e *Environment) GetSources() []interactions.Source { + if e == nil { + return nil + } + return e.Sources +} + +func (e *Environment) GetStatus() *Status { + if e == nil { + return nil + } + return e.Status +} + +func (e *Environment) GetUpdated() *string { + if e == nil { + return nil + } + return e.Updated +} diff --git a/internal/sdk/models/environments/environmentfile.go b/internal/sdk/models/environments/environmentfile.go new file mode 100644 index 0000000..399b8ee --- /dev/null +++ b/internal/sdk/models/environments/environmentfile.go @@ -0,0 +1,128 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Type - Output only. The type of the entry. +type Type string + +const ( + TypeFile Type = "file" + TypeDirectory Type = "directory" +) + +func (e Type) ToPointer() *Type { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Type) IsExact() bool { + if e != nil { + switch *e { + case "file", "directory": + return true + } + } + return false +} + +// EnvironmentFile - Metadata for a file or directory within an environment. +type EnvironmentFile struct { + // Output only. The creation time of the file/directory. + Created *time.Time `json:"created,omitzero"` + // Output only. The MIME type of the file (e.g., "text/python", "image/png"). + // Empty for directories. + // NOLINT + MimeType *string `json:"mime_type,omitzero"` + // Output only. The modification time of the file/directory. + Modified *time.Time `json:"modified,omitzero"` + // Output only. The name of the file or directory (e.g., "main.py" or "src"). + Name *string `json:"name,omitzero"` + // Output only. The full relative path within the environment + // (e.g., "workspace/src/main.py"). + Path *string `json:"path,omitzero"` + // Output only. The size of the file/directory in bytes. + // NOLINT + SizeBytes *string `json:"size_bytes,omitzero"` + // Output only. The type of the entry. + Type *Type `json:"type,omitzero"` +} + +func (e EnvironmentFile) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *EnvironmentFile) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *EnvironmentFile) GetCreated() *time.Time { + if e == nil { + return nil + } + return e.Created +} + +func (e *EnvironmentFile) GetMimeType() *string { + if e == nil { + return nil + } + return e.MimeType +} + +func (e *EnvironmentFile) GetModified() *time.Time { + if e == nil { + return nil + } + return e.Modified +} + +func (e *EnvironmentFile) GetName() *string { + if e == nil { + return nil + } + return e.Name +} + +func (e *EnvironmentFile) GetPath() *string { + if e == nil { + return nil + } + return e.Path +} + +func (e *EnvironmentFile) GetSizeBytes() *string { + if e == nil { + return nil + } + return e.SizeBytes +} + +func (e *EnvironmentFile) GetType() *Type { + if e == nil { + return nil + } + return e.Type +} diff --git a/internal/sdk/models/environments/getenvironmentfilesresponse.go b/internal/sdk/models/environments/getenvironmentfilesresponse.go new file mode 100644 index 0000000..a220831 --- /dev/null +++ b/internal/sdk/models/environments/getenvironmentfilesresponse.go @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GetEnvironmentFilesResponse - Response for `GetEnvironmentFiles`. +type GetEnvironmentFilesResponse struct { + // If the requested path is a directory, this contains its contents. + // If the requested path is a file, this contains a single entry with the + // file's metadata. + // If alt=media was specified, this is empty (content is served via `blob`). + Files []EnvironmentFile `json:"files,omitzero"` + // Pagination token for directory listing. + // NOLINT + NextPageToken *string `json:"next_page_token,omitzero"` +} + +func (g GetEnvironmentFilesResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentFilesResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentFilesResponse) GetFiles() []EnvironmentFile { + if g == nil { + return nil + } + return g.Files +} + +func (g *GetEnvironmentFilesResponse) GetNextPageToken() *string { + if g == nil { + return nil + } + return g.NextPageToken +} diff --git a/internal/sdk/models/environments/listenvironmentsresponse.go b/internal/sdk/models/environments/listenvironmentsresponse.go new file mode 100644 index 0000000..670cf76 --- /dev/null +++ b/internal/sdk/models/environments/listenvironmentsresponse.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package environments + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ListEnvironmentsResponse - Response for `ListEnvironments`. +type ListEnvironmentsResponse struct { + // Environments belonging to the provided project. + Environments []Environment `json:"environments,omitzero"` + // Pagination token. + NextPageToken *string `json:"next_page_token,omitzero"` +} + +func (l ListEnvironmentsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListEnvironmentsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListEnvironmentsResponse) GetEnvironments() []Environment { + if l == nil { + return nil + } + return l.Environments +} + +func (l *ListEnvironmentsResponse) GetNextPageToken() *string { + if l == nil { + return nil + } + return l.NextPageToken +} diff --git a/internal/sdk/models/genai/empty.go b/internal/sdk/models/genai/empty.go new file mode 100644 index 0000000..9db1b41 --- /dev/null +++ b/internal/sdk/models/genai/empty.go @@ -0,0 +1,21 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +// Empty - A generic empty message that you can re-use to avoid defining duplicated empty messages in your APIs. A typical example is to use it as the request or the response type of an API method. For instance: service Foo { rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); } +type Empty struct { +} diff --git a/internal/sdk/models/genai/file.go b/internal/sdk/models/genai/file.go new file mode 100644 index 0000000..e662a48 --- /dev/null +++ b/internal/sdk/models/genai/file.go @@ -0,0 +1,222 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Source of the File. +type Source string + +const ( + // SourceSourceUnspecified Used if source is not specified. + SourceSourceUnspecified Source = "SOURCE_UNSPECIFIED" + // SourceUploaded Indicates the file is uploaded by the user. + SourceUploaded Source = "UPLOADED" + // SourceGenerated Indicates the file is generated by Google. + SourceGenerated Source = "GENERATED" + // SourceRegistered Indicates the file is a registered, i.e. a Google Cloud Storage file. + SourceRegistered Source = "REGISTERED" +) + +func (e Source) ToPointer() *Source { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Source) IsExact() bool { + if e != nil { + switch *e { + case "SOURCE_UNSPECIFIED", "UPLOADED", "GENERATED", "REGISTERED": + return true + } + } + return false +} + +// State - Output only. Processing state of the File. +type State string + +const ( + // StateStateUnspecified The default value. This value is used if the state is omitted. + StateStateUnspecified State = "STATE_UNSPECIFIED" + // StateProcessing File is being processed and cannot be used for inference yet. + StateProcessing State = "PROCESSING" + // StateActive File is processed and available for inference. + StateActive State = "ACTIVE" + // StateFailed File failed processing. + StateFailed State = "FAILED" +) + +func (e State) ToPointer() *State { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *State) IsExact() bool { + if e != nil { + switch *e { + case "STATE_UNSPECIFIED", "PROCESSING", "ACTIVE", "FAILED": + return true + } + } + return false +} + +// File - A file uploaded to the API. Next ID: 15 +type File struct { + // Output only. The timestamp of when the `File` was created. + CreateTime *time.Time `json:"createTime,omitzero"` + // Optional. The human-readable display name for the `File`. The display name must be no more than 512 characters in length, including spaces. Example: "Welcome Image" + DisplayName *string `json:"displayName,omitzero"` + // Output only. The download uri of the `File`. + DownloadURI *string `json:"downloadUri,omitzero"` + // The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors). + Error *Status `json:"error,omitzero"` + // Output only. The timestamp of when the `File` will be deleted. Only set if the `File` is scheduled to expire. + ExpirationTime *time.Time `json:"expirationTime,omitzero"` + // Output only. MIME type of the file. + MimeType *string `json:"mimeType,omitzero"` + // Immutable. Identifier. The `File` resource name. The ID (name excluding the "files/" prefix) can contain up to 40 characters that are lowercase alphanumeric or dashes (-). The ID cannot start or end with a dash. If the name is empty on create, a unique name will be generated. Example: `files/123-456` + Name *string `json:"name,omitzero"` + // Output only. SHA-256 hash of the uploaded bytes. + Sha256Hash *string `json:"sha256Hash,omitzero"` + // Output only. Size of the file in bytes. + SizeBytes *string `json:"sizeBytes,omitzero"` + // Source of the File. + Source *Source `json:"source,omitzero"` + // Output only. Processing state of the File. + State *State `json:"state,omitzero"` + // Output only. The timestamp of when the `File` was last updated. + UpdateTime *time.Time `json:"updateTime,omitzero"` + // Output only. The uri of the `File`. + URI *string `json:"uri,omitzero"` + // Metadata for a video `File`. + VideoMetadata *VideoFileMetadata `json:"videoMetadata,omitzero"` +} + +func (f File) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *File) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *File) GetCreateTime() *time.Time { + if f == nil { + return nil + } + return f.CreateTime +} + +func (f *File) GetDisplayName() *string { + if f == nil { + return nil + } + return f.DisplayName +} + +func (f *File) GetDownloadURI() *string { + if f == nil { + return nil + } + return f.DownloadURI +} + +func (f *File) GetError() *Status { + if f == nil { + return nil + } + return f.Error +} + +func (f *File) GetExpirationTime() *time.Time { + if f == nil { + return nil + } + return f.ExpirationTime +} + +func (f *File) GetMimeType() *string { + if f == nil { + return nil + } + return f.MimeType +} + +func (f *File) GetName() *string { + if f == nil { + return nil + } + return f.Name +} + +func (f *File) GetSha256Hash() *string { + if f == nil { + return nil + } + return f.Sha256Hash +} + +func (f *File) GetSizeBytes() *string { + if f == nil { + return nil + } + return f.SizeBytes +} + +func (f *File) GetSource() *Source { + if f == nil { + return nil + } + return f.Source +} + +func (f *File) GetState() *State { + if f == nil { + return nil + } + return f.State +} + +func (f *File) GetUpdateTime() *time.Time { + if f == nil { + return nil + } + return f.UpdateTime +} + +func (f *File) GetURI() *string { + if f == nil { + return nil + } + return f.URI +} + +func (f *File) GetVideoMetadata() *VideoFileMetadata { + if f == nil { + return nil + } + return f.VideoMetadata +} diff --git a/internal/sdk/models/genai/listfilesresponse.go b/internal/sdk/models/genai/listfilesresponse.go new file mode 100644 index 0000000..524564e --- /dev/null +++ b/internal/sdk/models/genai/listfilesresponse.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ListFilesResponse - Response for `ListFiles`. +type ListFilesResponse struct { + // The list of `File`s. + Files []File `json:"files,omitzero"` + // A token that can be sent as a `page_token` into a subsequent `ListFiles` call. + NextPageToken *string `json:"nextPageToken,omitzero"` +} + +func (l ListFilesResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListFilesResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListFilesResponse) GetFiles() []File { + if l == nil { + return nil + } + return l.Files +} + +func (l *ListFilesResponse) GetNextPageToken() *string { + if l == nil { + return nil + } + return l.NextPageToken +} diff --git a/internal/sdk/models/genai/listmodelsresponse.go b/internal/sdk/models/genai/listmodelsresponse.go new file mode 100644 index 0000000..bd0a37a --- /dev/null +++ b/internal/sdk/models/genai/listmodelsresponse.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ListModelsResponse - Response from `ListModel` containing a paginated list of Models. +type ListModelsResponse struct { + // The returned Models. + Models []Model `json:"models,omitzero"` + // A token, which can be sent as `page_token` to retrieve the next page. If this field is omitted, there are no more pages. + NextPageToken *string `json:"nextPageToken,omitzero"` +} + +func (l ListModelsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListModelsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListModelsResponse) GetModels() []Model { + if l == nil { + return nil + } + return l.Models +} + +func (l *ListModelsResponse) GetNextPageToken() *string { + if l == nil { + return nil + } + return l.NextPageToken +} diff --git a/internal/sdk/models/genai/model.go b/internal/sdk/models/genai/model.go new file mode 100644 index 0000000..2d993dc --- /dev/null +++ b/internal/sdk/models/genai/model.go @@ -0,0 +1,153 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Model - Information about a Generative Language Model. +type Model struct { + // Required. The name of the base model, pass this to the generation request. Examples: * `gemini-1.5-flash` + BaseModelID *string `json:"baseModelId,omitzero"` + // A short description of the model. + Description *string `json:"description,omitzero"` + // The human-readable name of the model. E.g. "Gemini 1.5 Flash". The name can be up to 128 characters long and can consist of any UTF-8 characters. + DisplayName *string `json:"displayName,omitzero"` + // Maximum number of input tokens allowed for this model. + InputTokenLimit *int `json:"inputTokenLimit,omitzero"` + // The maximum temperature this model can use. + MaxTemperature *float32 `json:"maxTemperature,omitzero"` + // Required. The resource name of the `Model`. Refer to [Model variants](https://ai.google.dev/gemini-api/docs/models/gemini#model-variations) for all allowed values. Format: `models/{model}` with a `{model}` naming convention of: * "{base_model_id}-{version}" Examples: * `models/gemini-1.5-flash-001` + Name *string `json:"name,omitzero"` + // Maximum number of output tokens available for this model. + OutputTokenLimit *int `json:"outputTokenLimit,omitzero"` + // The model's supported generation methods. The corresponding API method names are defined as Pascal case strings, such as `generateMessage` and `generateContent`. + SupportedGenerationMethods []string `json:"supportedGenerationMethods,omitzero"` + // Controls the randomness of the output. Values can range over `[0.0,max_temperature]`, inclusive. A higher value will produce responses that are more varied, while a value closer to `0.0` will typically result in less surprising responses from the model. This value specifies default to be used by the backend while making the call to the model. + Temperature *float32 `json:"temperature,omitzero"` + // Whether the model supports thinking. + Thinking *bool `json:"thinking,omitzero"` + // For Top-k sampling. Top-k sampling considers the set of `top_k` most probable tokens. This value specifies default to be used by the backend while making the call to the model. If empty, indicates the model doesn't use top-k sampling, and `top_k` isn't allowed as a generation parameter. + TopK *int `json:"topK,omitzero"` + // For [Nucleus sampling](https://ai.google.dev/gemini-api/docs/prompting-strategies#top-p). Nucleus sampling considers the smallest set of tokens whose probability sum is at least `top_p`. This value specifies default to be used by the backend while making the call to the model. + TopP *float32 `json:"topP,omitzero"` + // Required. The version number of the model. This represents the major version (`1.0` or `1.5`) + Version *string `json:"version,omitzero"` +} + +func (m Model) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *Model) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *Model) GetBaseModelID() *string { + if m == nil { + return nil + } + return m.BaseModelID +} + +func (m *Model) GetDescription() *string { + if m == nil { + return nil + } + return m.Description +} + +func (m *Model) GetDisplayName() *string { + if m == nil { + return nil + } + return m.DisplayName +} + +func (m *Model) GetInputTokenLimit() *int { + if m == nil { + return nil + } + return m.InputTokenLimit +} + +func (m *Model) GetMaxTemperature() *float32 { + if m == nil { + return nil + } + return m.MaxTemperature +} + +func (m *Model) GetName() *string { + if m == nil { + return nil + } + return m.Name +} + +func (m *Model) GetOutputTokenLimit() *int { + if m == nil { + return nil + } + return m.OutputTokenLimit +} + +func (m *Model) GetSupportedGenerationMethods() []string { + if m == nil { + return nil + } + return m.SupportedGenerationMethods +} + +func (m *Model) GetTemperature() *float32 { + if m == nil { + return nil + } + return m.Temperature +} + +func (m *Model) GetThinking() *bool { + if m == nil { + return nil + } + return m.Thinking +} + +func (m *Model) GetTopK() *int { + if m == nil { + return nil + } + return m.TopK +} + +func (m *Model) GetTopP() *float32 { + if m == nil { + return nil + } + return m.TopP +} + +func (m *Model) GetVersion() *string { + if m == nil { + return nil + } + return m.Version +} diff --git a/internal/sdk/models/genai/registerfilesrequest.go b/internal/sdk/models/genai/registerfilesrequest.go new file mode 100644 index 0000000..3f5db02 --- /dev/null +++ b/internal/sdk/models/genai/registerfilesrequest.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RegisterFilesRequest - Request for `RegisterFiles`. +type RegisterFilesRequest struct { + // Required. The Google Cloud Storage URIs to register. Example: `gs://bucket/object`. + Uris []string `json:"uris,omitzero"` +} + +func (r RegisterFilesRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RegisterFilesRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RegisterFilesRequest) GetUris() []string { + if r == nil { + return nil + } + return r.Uris +} diff --git a/internal/sdk/models/genai/registerfilesresponse.go b/internal/sdk/models/genai/registerfilesresponse.go new file mode 100644 index 0000000..031c838 --- /dev/null +++ b/internal/sdk/models/genai/registerfilesresponse.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RegisterFilesResponse - Response for `RegisterFiles`. +type RegisterFilesResponse struct { + // The registered files to be used when calling GenerateContent. + Files []File `json:"files,omitzero"` +} + +func (r RegisterFilesResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RegisterFilesResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RegisterFilesResponse) GetFiles() []File { + if r == nil { + return nil + } + return r.Files +} diff --git a/internal/sdk/models/genai/status.go b/internal/sdk/models/genai/status.go new file mode 100644 index 0000000..ef03a10 --- /dev/null +++ b/internal/sdk/models/genai/status.go @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package genai + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Status - The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors). +type Status struct { + // The status code, which should be an enum value of google.rpc.Code. + Code *int `json:"code,omitzero"` + // A list of messages that carry the error details. There is a common set of message types for APIs to use. + Details []map[string]any `json:"details,omitzero"` + // A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client. + Message *string `json:"message,omitzero"` +} + +func (s Status) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *Status) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *Status) GetCode() *int { + if s == nil { + return nil + } + return s.Code +} + +func (s *Status) GetDetails() []map[string]any { + if s == nil { + return nil + } + return s.Details +} + +func (s *Status) GetMessage() *string { + if s == nil { + return nil + } + return s.Message +} diff --git a/src/commands/files/index.ts b/internal/sdk/models/genai/videofilemetadata.go similarity index 56% rename from src/commands/files/index.ts rename to internal/sdk/models/genai/videofilemetadata.go index df53717..61e5434 100644 --- a/src/commands/files/index.ts +++ b/internal/sdk/models/genai/videofilemetadata.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -12,17 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -// gemini-api files subcommand group -// TODO: Implement — see tasks/task_11.md +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. -import { defineCommand } from "citty"; +package genai -export default defineCommand({ - meta: { - name: "files", - description: "Manage environment files: download", - }, - subCommands: { - download: () => import("./download").then((m) => m.default), - }, -}); +// VideoFileMetadata - Metadata for a video `File`. +type VideoFileMetadata struct { + // Duration of the video. + VideoDuration *string `json:"videoDuration,omitzero"` +} + +func (v *VideoFileMetadata) GetVideoDuration() *string { + if v == nil { + return nil + } + return v.VideoDuration +} diff --git a/internal/sdk/models/interactions/agentoption.go b/internal/sdk/models/interactions/agentoption.go new file mode 100644 index 0000000..528cd25 --- /dev/null +++ b/internal/sdk/models/interactions/agentoption.go @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +// AgentOption - The agent to interact with. +type AgentOption string + +const ( + // AgentOptionDeepResearchProPreview122025 Gemini Deep Research Agent + AgentOptionDeepResearchProPreview122025 AgentOption = "deep-research-pro-preview-12-2025" + // AgentOptionDeepResearchPreview042026 Gemini Deep Research Agent + AgentOptionDeepResearchPreview042026 AgentOption = "deep-research-preview-04-2026" + // AgentOptionDeepResearchMaxPreview042026 Gemini Deep Research Max Agent + AgentOptionDeepResearchMaxPreview042026 AgentOption = "deep-research-max-preview-04-2026" + // AgentOptionAntigravityPreview052026 Use the Antigravity managed agent to perform multi-step tasks that require reasoning, file operations, and tool use. + AgentOptionAntigravityPreview052026 AgentOption = "antigravity-preview-05-2026" +) + +func (e AgentOption) ToPointer() *AgentOption { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *AgentOption) IsExact() bool { + if e != nil { + switch *e { + case "deep-research-pro-preview-12-2025", "deep-research-preview-04-2026", "deep-research-max-preview-04-2026", "antigravity-preview-05-2026": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/allowedtools.go b/internal/sdk/models/interactions/allowedtools.go new file mode 100644 index 0000000..223db52 --- /dev/null +++ b/internal/sdk/models/interactions/allowedtools.go @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// AllowedTools - The configuration for allowed tools. +type AllowedTools struct { + Mode *ToolChoiceType `json:"mode,omitzero"` + // The names of the allowed tools. + Tools []string `json:"tools,omitzero"` +} + +func (a AllowedTools) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AllowedTools) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AllowedTools) GetMode() *ToolChoiceType { + if a == nil { + return nil + } + return a.Mode +} + +func (a *AllowedTools) GetTools() []string { + if a == nil { + return nil + } + return a.Tools +} diff --git a/internal/sdk/models/interactions/allowlistentry.go b/internal/sdk/models/interactions/allowlistentry.go new file mode 100644 index 0000000..cd5b6c7 --- /dev/null +++ b/internal/sdk/models/interactions/allowlistentry.go @@ -0,0 +1,183 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type TransformType string + +const ( + TransformTypeArrayOfMapOfStr TransformType = "arrayOfMapOfStr" + TransformTypeMapOfStr TransformType = "mapOfStr" + TransformTypeUnknown TransformType = "Unknown" +) + +// Transform - Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically. +type Transform struct { + ArrayOfMapOfStr []map[string]string `queryParam:"inline" union:"member"` + MapOfStr map[string]string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type TransformType +} + +func CreateTransformArrayOfMapOfStr(arrayOfMapOfStr []map[string]string) Transform { + typ := TransformTypeArrayOfMapOfStr + + return Transform{ + ArrayOfMapOfStr: arrayOfMapOfStr, + Type: typ, + } +} + +func CreateTransformMapOfStr(mapOfStr map[string]string) Transform { + typ := TransformTypeMapOfStr + + return Transform{ + MapOfStr: mapOfStr, + Type: typ, + } +} + +func CreateTransformUnknown(raw json.RawMessage) Transform { + return Transform{ + UnknownRaw: raw, + Type: TransformTypeUnknown, + } +} + +func (u Transform) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Transform) IsUnknown() bool { + return u.Type == TransformTypeUnknown +} + +func (u *Transform) UnmarshalJSON(data []byte) error { + *u = Transform{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var arrayOfMapOfStr []map[string]string = []map[string]string{} + if err := utils.UnmarshalJSON(data, &arrayOfMapOfStr, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: TransformTypeArrayOfMapOfStr, + Value: arrayOfMapOfStr, + }) + } + + var mapOfStr map[string]string = map[string]string{} + if err := utils.UnmarshalJSON(data, &mapOfStr, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: TransformTypeMapOfStr, + Value: mapOfStr, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = TransformTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = TransformTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(TransformType) + switch best.Type { + case TransformTypeArrayOfMapOfStr: + u.ArrayOfMapOfStr = best.Value.([]map[string]string) + return nil + case TransformTypeMapOfStr: + u.MapOfStr = best.Value.(map[string]string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = TransformTypeUnknown + return nil +} + +func (u Transform) MarshalJSON() ([]byte, error) { + if u.ArrayOfMapOfStr != nil { + return utils.MarshalJSON(u.ArrayOfMapOfStr, "", true) + } + + if u.MapOfStr != nil { + return utils.MarshalJSON(u.MapOfStr, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Transform: all fields are null") +} + +// AllowlistEntry - A single domain allowlist rule with optional header injection. +type AllowlistEntry struct { + // Optional. Reference to a server-managed Credential resource by ID. + Credential *string `json:"credential,omitzero"` + // Domain to allow outbound requests to. Supports wildcards (e.g. '*.googleapis.com'). Use '*' to allow all domains. + Domain string `json:"domain"` + // Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically. + Transform *Transform `json:"transform,omitzero"` +} + +func (a AllowlistEntry) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AllowlistEntry) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AllowlistEntry) GetCredential() *string { + if a == nil { + return nil + } + return a.Credential +} + +func (a *AllowlistEntry) GetDomain() string { + if a == nil { + return "" + } + return a.Domain +} + +func (a *AllowlistEntry) GetTransform() *Transform { + if a == nil { + return nil + } + return a.Transform +} diff --git a/internal/sdk/models/interactions/annotation.go b/internal/sdk/models/interactions/annotation.go new file mode 100644 index 0000000..6243832 --- /dev/null +++ b/internal/sdk/models/interactions/annotation.go @@ -0,0 +1,190 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type AnnotationType string + +const ( + AnnotationTypeFileCitation AnnotationType = "file_citation" + AnnotationTypePlaceCitation AnnotationType = "place_citation" + AnnotationTypeURLCitation AnnotationType = "url_citation" + AnnotationTypeWordInfo AnnotationType = "word_info" + AnnotationTypeUnknown AnnotationType = "UNKNOWN" +) + +// Annotation - Citation information for model-generated content. +type Annotation struct { + FileCitation *FileCitation `queryParam:"inline" union:"member"` + PlaceCitation *PlaceCitation `queryParam:"inline" union:"member"` + URLCitation *URLCitation `queryParam:"inline" union:"member"` + WordInfo *WordInfo `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type AnnotationType +} + +func CreateAnnotationFileCitation(fileCitation FileCitation) Annotation { + typ := AnnotationTypeFileCitation + + return Annotation{ + FileCitation: &fileCitation, + Type: typ, + } +} + +func CreateAnnotationPlaceCitation(placeCitation PlaceCitation) Annotation { + typ := AnnotationTypePlaceCitation + + return Annotation{ + PlaceCitation: &placeCitation, + Type: typ, + } +} + +func CreateAnnotationURLCitation(urlCitation URLCitation) Annotation { + typ := AnnotationTypeURLCitation + + return Annotation{ + URLCitation: &urlCitation, + Type: typ, + } +} + +func CreateAnnotationWordInfo(wordInfo WordInfo) Annotation { + typ := AnnotationTypeWordInfo + + return Annotation{ + WordInfo: &wordInfo, + Type: typ, + } +} + +func CreateAnnotationUnknown(raw json.RawMessage) Annotation { + return Annotation{ + UnknownRaw: raw, + Type: AnnotationTypeUnknown, + } +} + +func (u Annotation) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Annotation) IsUnknown() bool { + return u.Type == AnnotationTypeUnknown +} + +func (u *Annotation) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = Annotation{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = AnnotationTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = AnnotationTypeUnknown + return nil + } + + switch dis.Type { + case "file_citation": + fileCitation := new(FileCitation) + if err := utils.UnmarshalJSON(data, &fileCitation, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == file_citation) type FileCitation within Annotation: %w", string(data), err) + } + + u.FileCitation = fileCitation + u.Type = AnnotationTypeFileCitation + return nil + case "place_citation": + placeCitation := new(PlaceCitation) + if err := utils.UnmarshalJSON(data, &placeCitation, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == place_citation) type PlaceCitation within Annotation: %w", string(data), err) + } + + u.PlaceCitation = placeCitation + u.Type = AnnotationTypePlaceCitation + return nil + case "url_citation": + urlCitation := new(URLCitation) + if err := utils.UnmarshalJSON(data, &urlCitation, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_citation) type URLCitation within Annotation: %w", string(data), err) + } + + u.URLCitation = urlCitation + u.Type = AnnotationTypeURLCitation + return nil + case "word_info": + wordInfo := new(WordInfo) + if err := utils.UnmarshalJSON(data, &wordInfo, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == word_info) type WordInfo within Annotation: %w", string(data), err) + } + + u.WordInfo = wordInfo + u.Type = AnnotationTypeWordInfo + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = AnnotationTypeUnknown + return nil + } + +} + +func (u Annotation) MarshalJSON() ([]byte, error) { + if u.FileCitation != nil { + return utils.MarshalJSON(u.FileCitation, "", true) + } + + if u.PlaceCitation != nil { + return utils.MarshalJSON(u.PlaceCitation, "", true) + } + + if u.URLCitation != nil { + return utils.MarshalJSON(u.URLCitation, "", true) + } + + if u.WordInfo != nil { + return utils.MarshalJSON(u.WordInfo, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Annotation: all fields are null") +} diff --git a/internal/sdk/models/interactions/antigravityagentconfig.go b/internal/sdk/models/interactions/antigravityagentconfig.go new file mode 100644 index 0000000..70e0826 --- /dev/null +++ b/internal/sdk/models/interactions/antigravityagentconfig.go @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// AntigravityAgentConfig - Configuration for the Antigravity agent runtime. +// Provides server-side control over the agent's execution environment +// and tool configuration. +type AntigravityAgentConfig struct { + // Max total tokens for the agent run. + MaxTotalTokens *string `json:"max_total_tokens,omitzero"` + // The model to use for agent reasoning. + Model *string `json:"model,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"antigravity" json:"type"` +} + +func (a AntigravityAgentConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AntigravityAgentConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AntigravityAgentConfig) GetMaxTotalTokens() *string { + if a == nil { + return nil + } + return a.MaxTotalTokens +} + +func (a *AntigravityAgentConfig) GetModel() *string { + if a == nil { + return nil + } + return a.Model +} + +func (a *AntigravityAgentConfig) GetType() string { + return "antigravity" +} diff --git a/internal/sdk/models/interactions/argumentsdelta.go b/internal/sdk/models/interactions/argumentsdelta.go new file mode 100644 index 0000000..9470e39 --- /dev/null +++ b/internal/sdk/models/interactions/argumentsdelta.go @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ArgumentsDelta struct { + Arguments *string `json:"arguments,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"arguments_delta" json:"type"` +} + +func (a ArgumentsDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *ArgumentsDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *ArgumentsDelta) GetArguments() *string { + if a == nil { + return nil + } + return a.Arguments +} + +func (a *ArgumentsDelta) GetType() string { + return "arguments_delta" +} diff --git a/internal/sdk/models/interactions/audiocontent.go b/internal/sdk/models/interactions/audiocontent.go new file mode 100644 index 0000000..c28585a --- /dev/null +++ b/internal/sdk/models/interactions/audiocontent.go @@ -0,0 +1,121 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// AudioContentMimeType - The mime type of the audio. +type AudioContentMimeType string + +const ( + AudioContentMimeTypeAudioWav AudioContentMimeType = "audio/wav" + AudioContentMimeTypeAudioMp3 AudioContentMimeType = "audio/mp3" + AudioContentMimeTypeAudioAiff AudioContentMimeType = "audio/aiff" + AudioContentMimeTypeAudioAac AudioContentMimeType = "audio/aac" + AudioContentMimeTypeAudioOgg AudioContentMimeType = "audio/ogg" + AudioContentMimeTypeAudioFlac AudioContentMimeType = "audio/flac" + AudioContentMimeTypeAudioMpeg AudioContentMimeType = "audio/mpeg" + AudioContentMimeTypeAudioM4a AudioContentMimeType = "audio/m4a" + AudioContentMimeTypeAudioL16 AudioContentMimeType = "audio/l16" + AudioContentMimeTypeAudioOpus AudioContentMimeType = "audio/opus" + AudioContentMimeTypeAudioAlaw AudioContentMimeType = "audio/alaw" + AudioContentMimeTypeAudioMulaw AudioContentMimeType = "audio/mulaw" + AudioContentMimeTypeAudioWebm AudioContentMimeType = "audio/webm" +) + +func (e AudioContentMimeType) ToPointer() *AudioContentMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *AudioContentMimeType) IsExact() bool { + if e != nil { + switch *e { + case "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", "audio/alaw", "audio/mulaw", "audio/webm": + return true + } + } + return false +} + +// AudioContent - An audio content block. +type AudioContent struct { + // The number of audio channels. + Channels *int `json:"channels,omitzero"` + // The audio content. + Data *string `json:"data,omitzero"` + // The mime type of the audio. + MimeType *AudioContentMimeType `json:"mime_type,omitzero"` + // The sample rate of the audio. + SampleRate *int `json:"sample_rate,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"audio" json:"type"` + // The URI of the audio. + URI *string `json:"uri,omitzero"` +} + +func (a AudioContent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AudioContent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AudioContent) GetChannels() *int { + if a == nil { + return nil + } + return a.Channels +} + +func (a *AudioContent) GetData() *string { + if a == nil { + return nil + } + return a.Data +} + +func (a *AudioContent) GetMimeType() *AudioContentMimeType { + if a == nil { + return nil + } + return a.MimeType +} + +func (a *AudioContent) GetSampleRate() *int { + if a == nil { + return nil + } + return a.SampleRate +} + +func (a *AudioContent) GetType() string { + return "audio" +} + +func (a *AudioContent) GetURI() *string { + if a == nil { + return nil + } + return a.URI +} diff --git a/internal/sdk/models/interactions/audiodelta.go b/internal/sdk/models/interactions/audiodelta.go new file mode 100644 index 0000000..e486b7c --- /dev/null +++ b/internal/sdk/models/interactions/audiodelta.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type AudioDeltaMimeType string + +const ( + AudioDeltaMimeTypeAudioWav AudioDeltaMimeType = "audio/wav" + AudioDeltaMimeTypeAudioMp3 AudioDeltaMimeType = "audio/mp3" + AudioDeltaMimeTypeAudioAiff AudioDeltaMimeType = "audio/aiff" + AudioDeltaMimeTypeAudioAac AudioDeltaMimeType = "audio/aac" + AudioDeltaMimeTypeAudioOgg AudioDeltaMimeType = "audio/ogg" + AudioDeltaMimeTypeAudioFlac AudioDeltaMimeType = "audio/flac" + AudioDeltaMimeTypeAudioMpeg AudioDeltaMimeType = "audio/mpeg" + AudioDeltaMimeTypeAudioM4a AudioDeltaMimeType = "audio/m4a" + AudioDeltaMimeTypeAudioL16 AudioDeltaMimeType = "audio/l16" + AudioDeltaMimeTypeAudioOpus AudioDeltaMimeType = "audio/opus" + AudioDeltaMimeTypeAudioAlaw AudioDeltaMimeType = "audio/alaw" + AudioDeltaMimeTypeAudioMulaw AudioDeltaMimeType = "audio/mulaw" + AudioDeltaMimeTypeAudioWebm AudioDeltaMimeType = "audio/webm" +) + +func (e AudioDeltaMimeType) ToPointer() *AudioDeltaMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *AudioDeltaMimeType) IsExact() bool { + if e != nil { + switch *e { + case "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", "audio/alaw", "audio/mulaw", "audio/webm": + return true + } + } + return false +} + +type AudioDelta struct { + // The number of audio channels. + Channels *int `json:"channels,omitzero"` + Data *string `json:"data,omitzero"` + MimeType *AudioDeltaMimeType `json:"mime_type,omitzero"` + // Deprecated. Use sample_rate instead. The value is ignored. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + Rate *int `json:"rate,omitzero"` + // The sample rate of the audio. + SampleRate *int `json:"sample_rate,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"audio" json:"type"` + URI *string `json:"uri,omitzero"` +} + +func (a AudioDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AudioDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AudioDelta) GetChannels() *int { + if a == nil { + return nil + } + return a.Channels +} + +func (a *AudioDelta) GetData() *string { + if a == nil { + return nil + } + return a.Data +} + +func (a *AudioDelta) GetMimeType() *AudioDeltaMimeType { + if a == nil { + return nil + } + return a.MimeType +} + +func (a *AudioDelta) GetRate() *int { + if a == nil { + return nil + } + return a.Rate +} + +func (a *AudioDelta) GetSampleRate() *int { + if a == nil { + return nil + } + return a.SampleRate +} + +func (a *AudioDelta) GetType() string { + return "audio" +} + +func (a *AudioDelta) GetURI() *string { + if a == nil { + return nil + } + return a.URI +} diff --git a/internal/sdk/models/interactions/audioresponseformat.go b/internal/sdk/models/interactions/audioresponseformat.go new file mode 100644 index 0000000..a6ef4a2 --- /dev/null +++ b/internal/sdk/models/interactions/audioresponseformat.go @@ -0,0 +1,129 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// AudioResponseFormatDelivery - The delivery mode for the audio output. +type AudioResponseFormatDelivery string + +const ( + AudioResponseFormatDeliveryInline AudioResponseFormatDelivery = "inline" + AudioResponseFormatDeliveryURI AudioResponseFormatDelivery = "uri" +) + +func (e AudioResponseFormatDelivery) ToPointer() *AudioResponseFormatDelivery { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *AudioResponseFormatDelivery) IsExact() bool { + if e != nil { + switch *e { + case "inline", "uri": + return true + } + } + return false +} + +// AudioResponseFormatMimeType - The MIME type of the audio output. +type AudioResponseFormatMimeType string + +const ( + AudioResponseFormatMimeTypeAudioMp3 AudioResponseFormatMimeType = "audio/mp3" + AudioResponseFormatMimeTypeAudioOggOpus AudioResponseFormatMimeType = "audio/ogg_opus" + AudioResponseFormatMimeTypeAudioL16 AudioResponseFormatMimeType = "audio/l16" + AudioResponseFormatMimeTypeAudioWav AudioResponseFormatMimeType = "audio/wav" + AudioResponseFormatMimeTypeAudioAlaw AudioResponseFormatMimeType = "audio/alaw" + AudioResponseFormatMimeTypeAudioMulaw AudioResponseFormatMimeType = "audio/mulaw" +) + +func (e AudioResponseFormatMimeType) ToPointer() *AudioResponseFormatMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *AudioResponseFormatMimeType) IsExact() bool { + if e != nil { + switch *e { + case "audio/mp3", "audio/ogg_opus", "audio/l16", "audio/wav", "audio/alaw", "audio/mulaw": + return true + } + } + return false +} + +// AudioResponseFormat - Configuration for audio output format. +type AudioResponseFormat struct { + // Bit rate in bits per second (bps). Only applicable for compressed formats + // (MP3, Opus). + BitRate *int `json:"bit_rate,omitzero"` + // The delivery mode for the audio output. + Delivery *AudioResponseFormatDelivery `json:"delivery,omitzero"` + // The MIME type of the audio output. + MimeType *AudioResponseFormatMimeType `json:"mime_type,omitzero"` + // Sample rate in Hz. + SampleRate *int `json:"sample_rate,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"audio" json:"type"` +} + +func (a AudioResponseFormat) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *AudioResponseFormat) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *AudioResponseFormat) GetBitRate() *int { + if a == nil { + return nil + } + return a.BitRate +} + +func (a *AudioResponseFormat) GetDelivery() *AudioResponseFormatDelivery { + if a == nil { + return nil + } + return a.Delivery +} + +func (a *AudioResponseFormat) GetMimeType() *AudioResponseFormatMimeType { + if a == nil { + return nil + } + return a.MimeType +} + +func (a *AudioResponseFormat) GetSampleRate() *int { + if a == nil { + return nil + } + return a.SampleRate +} + +func (a *AudioResponseFormat) GetType() string { + return "audio" +} diff --git a/internal/sdk/models/interactions/codeexecution.go b/internal/sdk/models/interactions/codeexecution.go new file mode 100644 index 0000000..1fc8d38 --- /dev/null +++ b/internal/sdk/models/interactions/codeexecution.go @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// CodeExecution - A tool that can be used by the model to execute code. +type CodeExecution struct { + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"code_execution" json:"type"` +} + +func (c CodeExecution) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeExecution) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeExecution) GetType() string { + return "code_execution" +} diff --git a/internal/sdk/models/interactions/codeexecutioncallarguments.go b/internal/sdk/models/interactions/codeexecutioncallarguments.go new file mode 100644 index 0000000..b534cad --- /dev/null +++ b/internal/sdk/models/interactions/codeexecutioncallarguments.go @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Language - Programming language of the `code`. +type Language string + +const ( + LanguagePython Language = "python" +) + +func (e Language) ToPointer() *Language { + return &e +} +func (e *Language) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "python": + *e = Language(v) + return nil + default: + return fmt.Errorf("invalid value for Language: %v", v) + } +} + +// CodeExecutionCallArguments - The arguments to pass to the code execution. +type CodeExecutionCallArguments struct { + // The code to be executed. + Code *string `json:"code,omitzero"` + // Programming language of the `code`. + Language *Language `json:"language,omitzero"` +} + +func (c CodeExecutionCallArguments) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeExecutionCallArguments) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeExecutionCallArguments) GetCode() *string { + if c == nil { + return nil + } + return c.Code +} + +func (c *CodeExecutionCallArguments) GetLanguage() *Language { + if c == nil { + return nil + } + return c.Language +} diff --git a/internal/sdk/models/interactions/codeexecutioncalldelta.go b/internal/sdk/models/interactions/codeexecutioncalldelta.go new file mode 100644 index 0000000..c7106b9 --- /dev/null +++ b/internal/sdk/models/interactions/codeexecutioncalldelta.go @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CodeExecutionCallDelta struct { + // The arguments to pass to the code execution. + Arguments CodeExecutionCallArguments `json:"arguments"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"code_execution_call" json:"type"` +} + +func (c CodeExecutionCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeExecutionCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeExecutionCallDelta) GetArguments() CodeExecutionCallArguments { + if c == nil { + return CodeExecutionCallArguments{} + } + return c.Arguments +} + +func (c *CodeExecutionCallDelta) GetSignature() *string { + if c == nil { + return nil + } + return c.Signature +} + +func (c *CodeExecutionCallDelta) GetType() string { + return "code_execution_call" +} diff --git a/internal/sdk/models/interactions/codeexecutioncallstep.go b/internal/sdk/models/interactions/codeexecutioncallstep.go new file mode 100644 index 0000000..96fb21e --- /dev/null +++ b/internal/sdk/models/interactions/codeexecutioncallstep.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// CodeExecutionCallStep - Code execution call step. +type CodeExecutionCallStep struct { + // The arguments to pass to the code execution. + Arguments CodeExecutionCallArguments `json:"arguments"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"code_execution_call" json:"type"` +} + +func (c CodeExecutionCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeExecutionCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeExecutionCallStep) GetArguments() CodeExecutionCallArguments { + if c == nil { + return CodeExecutionCallArguments{} + } + return c.Arguments +} + +func (c *CodeExecutionCallStep) GetID() string { + if c == nil { + return "" + } + return c.ID +} + +func (c *CodeExecutionCallStep) GetSignature() *string { + if c == nil { + return nil + } + return c.Signature +} + +func (c *CodeExecutionCallStep) GetType() string { + return "code_execution_call" +} diff --git a/internal/sdk/models/interactions/codeexecutionresultdelta.go b/internal/sdk/models/interactions/codeexecutionresultdelta.go new file mode 100644 index 0000000..c170a9b --- /dev/null +++ b/internal/sdk/models/interactions/codeexecutionresultdelta.go @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CodeExecutionResultDelta struct { + IsError *bool `json:"is_error,omitzero"` + Result string `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"code_execution_result" json:"type"` +} + +func (c CodeExecutionResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeExecutionResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeExecutionResultDelta) GetIsError() *bool { + if c == nil { + return nil + } + return c.IsError +} + +func (c *CodeExecutionResultDelta) GetResult() string { + if c == nil { + return "" + } + return c.Result +} + +func (c *CodeExecutionResultDelta) GetSignature() *string { + if c == nil { + return nil + } + return c.Signature +} + +func (c *CodeExecutionResultDelta) GetType() string { + return "code_execution_result" +} diff --git a/internal/sdk/models/interactions/codeexecutionresultstep.go b/internal/sdk/models/interactions/codeexecutionresultstep.go new file mode 100644 index 0000000..fcce250 --- /dev/null +++ b/internal/sdk/models/interactions/codeexecutionresultstep.go @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// CodeExecutionResultStep - Code execution result step. +type CodeExecutionResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // Whether the code execution resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // Required. The output of the code execution. + Result string `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"code_execution_result" json:"type"` +} + +func (c CodeExecutionResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeExecutionResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeExecutionResultStep) GetCallID() string { + if c == nil { + return "" + } + return c.CallID +} + +func (c *CodeExecutionResultStep) GetIsError() *bool { + if c == nil { + return nil + } + return c.IsError +} + +func (c *CodeExecutionResultStep) GetResult() string { + if c == nil { + return "" + } + return c.Result +} + +func (c *CodeExecutionResultStep) GetSignature() *string { + if c == nil { + return nil + } + return c.Signature +} + +func (c *CodeExecutionResultStep) GetType() string { + return "code_execution_result" +} diff --git a/internal/sdk/models/interactions/codemenderagentconfig.go b/internal/sdk/models/interactions/codemenderagentconfig.go new file mode 100644 index 0000000..96c0299 --- /dev/null +++ b/internal/sdk/models/interactions/codemenderagentconfig.go @@ -0,0 +1,91 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// CodeMenderAgentConfig - Configuration for the CodeMender agent. +type CodeMenderAgentConfig struct { + // Request parameters specific to FIND sessions, used for discovering + // vulnerabilities in a codebase. + FindRequest *FindRequest `json:"find_request,omitzero"` + // Request parameters specific to FIX sessions, used for generating and + // validating security patches. + FixRequest *FixRequest `json:"fix_request,omitzero"` + // The name of the model to use for the CodeMender agent. One + // CodeMender session will only use one model. + Model *string `json:"model,omitzero"` + // The configuration of CodeMender sessions. + SessionConfig *SessionConfig `json:"session_config,omitzero"` + // Parameter for grouping multiple interactions that belong to + // the same CodeMender session. + SessionID *string `json:"session_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"code-mender" json:"type"` +} + +func (c CodeMenderAgentConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CodeMenderAgentConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CodeMenderAgentConfig) GetFindRequest() *FindRequest { + if c == nil { + return nil + } + return c.FindRequest +} + +func (c *CodeMenderAgentConfig) GetFixRequest() *FixRequest { + if c == nil { + return nil + } + return c.FixRequest +} + +func (c *CodeMenderAgentConfig) GetModel() *string { + if c == nil { + return nil + } + return c.Model +} + +func (c *CodeMenderAgentConfig) GetSessionConfig() *SessionConfig { + if c == nil { + return nil + } + return c.SessionConfig +} + +func (c *CodeMenderAgentConfig) GetSessionID() *string { + if c == nil { + return nil + } + return c.SessionID +} + +func (c *CodeMenderAgentConfig) GetType() string { + return "code-mender" +} diff --git a/internal/sdk/models/interactions/computeruse.go b/internal/sdk/models/interactions/computeruse.go new file mode 100644 index 0000000..693834f --- /dev/null +++ b/internal/sdk/models/interactions/computeruse.go @@ -0,0 +1,130 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DisabledSafetyPolicy string + +const ( + DisabledSafetyPolicyFinancialTransactions DisabledSafetyPolicy = "financial_transactions" + DisabledSafetyPolicySensitiveDataModification DisabledSafetyPolicy = "sensitive_data_modification" + DisabledSafetyPolicyCommunicationTool DisabledSafetyPolicy = "communication_tool" + DisabledSafetyPolicyAccountCreation DisabledSafetyPolicy = "account_creation" + DisabledSafetyPolicyDataModification DisabledSafetyPolicy = "data_modification" + DisabledSafetyPolicyUserConsentManagement DisabledSafetyPolicy = "user_consent_management" + DisabledSafetyPolicyLegalTermsAndAgreements DisabledSafetyPolicy = "legal_terms_and_agreements" +) + +func (e DisabledSafetyPolicy) ToPointer() *DisabledSafetyPolicy { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *DisabledSafetyPolicy) IsExact() bool { + if e != nil { + switch *e { + case "financial_transactions", "sensitive_data_modification", "communication_tool", "account_creation", "data_modification", "user_consent_management", "legal_terms_and_agreements": + return true + } + } + return false +} + +// EnvironmentEnum - The environment being operated. +type EnvironmentEnum string + +const ( + EnvironmentEnumBrowser EnvironmentEnum = "browser" + EnvironmentEnumMobile EnvironmentEnum = "mobile" + EnvironmentEnumDesktop EnvironmentEnum = "desktop" +) + +func (e EnvironmentEnum) ToPointer() *EnvironmentEnum { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *EnvironmentEnum) IsExact() bool { + if e != nil { + switch *e { + case "browser", "mobile", "desktop": + return true + } + } + return false +} + +// ComputerUse - A tool that can be used by the model to interact with the computer. +type ComputerUse struct { + // Optional. Disabled safety policies for computer use. + DisabledSafetyPolicies []DisabledSafetyPolicy `json:"disabled_safety_policies,omitzero"` + // Whether enable the prompt injection detection check on computer-use + // request. + EnablePromptInjectionDetection *bool `json:"enable_prompt_injection_detection,omitzero"` + // The environment being operated. + Environment *EnvironmentEnum `json:"environment,omitzero"` + // The list of predefined functions that are excluded from the model call. + ExcludedPredefinedFunctions []string `json:"excluded_predefined_functions,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"computer_use" json:"type"` +} + +func (c ComputerUse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *ComputerUse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *ComputerUse) GetDisabledSafetyPolicies() []DisabledSafetyPolicy { + if c == nil { + return nil + } + return c.DisabledSafetyPolicies +} + +func (c *ComputerUse) GetEnablePromptInjectionDetection() *bool { + if c == nil { + return nil + } + return c.EnablePromptInjectionDetection +} + +func (c *ComputerUse) GetEnvironment() *EnvironmentEnum { + if c == nil { + return nil + } + return c.Environment +} + +func (c *ComputerUse) GetExcludedPredefinedFunctions() []string { + if c == nil { + return nil + } + return c.ExcludedPredefinedFunctions +} + +func (c *ComputerUse) GetType() string { + return "computer_use" +} diff --git a/internal/sdk/models/interactions/content.go b/internal/sdk/models/interactions/content.go new file mode 100644 index 0000000..7d5c1c5 --- /dev/null +++ b/internal/sdk/models/interactions/content.go @@ -0,0 +1,214 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ContentType string + +const ( + ContentTypeAudio ContentType = "audio" + ContentTypeDocument ContentType = "document" + ContentTypeImage ContentType = "image" + ContentTypeText ContentType = "text" + ContentTypeVideo ContentType = "video" + ContentTypeUnknown ContentType = "UNKNOWN" +) + +// Content - The content of the response. +type Content struct { + AudioContent *AudioContent `queryParam:"inline" union:"member"` + DocumentContent *DocumentContent `queryParam:"inline" union:"member"` + ImageContent *ImageContent `queryParam:"inline" union:"member"` + TextContent *TextContent `queryParam:"inline" union:"member"` + VideoContent *VideoContent `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type ContentType +} + +func CreateContentAudio(audio AudioContent) Content { + typ := ContentTypeAudio + + return Content{ + AudioContent: &audio, + Type: typ, + } +} + +func CreateContentDocument(document DocumentContent) Content { + typ := ContentTypeDocument + + return Content{ + DocumentContent: &document, + Type: typ, + } +} + +func CreateContentImage(image ImageContent) Content { + typ := ContentTypeImage + + return Content{ + ImageContent: &image, + Type: typ, + } +} + +func CreateContentText(text TextContent) Content { + typ := ContentTypeText + + return Content{ + TextContent: &text, + Type: typ, + } +} + +func CreateContentVideo(video VideoContent) Content { + typ := ContentTypeVideo + + return Content{ + VideoContent: &video, + Type: typ, + } +} + +func CreateContentUnknown(raw json.RawMessage) Content { + return Content{ + UnknownRaw: raw, + Type: ContentTypeUnknown, + } +} + +func (u Content) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Content) IsUnknown() bool { + return u.Type == ContentTypeUnknown +} + +func (u *Content) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = Content{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ContentTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ContentTypeUnknown + return nil + } + + switch dis.Type { + case "audio": + audioContent := new(AudioContent) + if err := utils.UnmarshalJSON(data, &audioContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == audio) type AudioContent within Content: %w", string(data), err) + } + + u.AudioContent = audioContent + u.Type = ContentTypeAudio + return nil + case "document": + documentContent := new(DocumentContent) + if err := utils.UnmarshalJSON(data, &documentContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == document) type DocumentContent within Content: %w", string(data), err) + } + + u.DocumentContent = documentContent + u.Type = ContentTypeDocument + return nil + case "image": + imageContent := new(ImageContent) + if err := utils.UnmarshalJSON(data, &imageContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == image) type ImageContent within Content: %w", string(data), err) + } + + u.ImageContent = imageContent + u.Type = ContentTypeImage + return nil + case "text": + textContent := new(TextContent) + if err := utils.UnmarshalJSON(data, &textContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == text) type TextContent within Content: %w", string(data), err) + } + + u.TextContent = textContent + u.Type = ContentTypeText + return nil + case "video": + videoContent := new(VideoContent) + if err := utils.UnmarshalJSON(data, &videoContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == video) type VideoContent within Content: %w", string(data), err) + } + + u.VideoContent = videoContent + u.Type = ContentTypeVideo + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = ContentTypeUnknown + return nil + } + +} + +func (u Content) MarshalJSON() ([]byte, error) { + if u.AudioContent != nil { + return utils.MarshalJSON(u.AudioContent, "", true) + } + + if u.DocumentContent != nil { + return utils.MarshalJSON(u.DocumentContent, "", true) + } + + if u.ImageContent != nil { + return utils.MarshalJSON(u.ImageContent, "", true) + } + + if u.TextContent != nil { + return utils.MarshalJSON(u.TextContent, "", true) + } + + if u.VideoContent != nil { + return utils.MarshalJSON(u.VideoContent, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Content: all fields are null") +} diff --git a/internal/sdk/models/interactions/createagentinteraction.go b/internal/sdk/models/interactions/createagentinteraction.go new file mode 100644 index 0000000..d04d3b6 --- /dev/null +++ b/internal/sdk/models/interactions/createagentinteraction.go @@ -0,0 +1,553 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateAgentInteractionAgentConfigType string + +const ( + CreateAgentInteractionAgentConfigTypeAntigravity CreateAgentInteractionAgentConfigType = "antigravity" + CreateAgentInteractionAgentConfigTypeCodeMender CreateAgentInteractionAgentConfigType = "code-mender" + CreateAgentInteractionAgentConfigTypeDeepResearch CreateAgentInteractionAgentConfigType = "deep-research" + CreateAgentInteractionAgentConfigTypeDynamic CreateAgentInteractionAgentConfigType = "dynamic" +) + +// CreateAgentInteractionAgentConfig - Configuration parameters for the agent interaction. +type CreateAgentInteractionAgentConfig struct { + AntigravityAgentConfig *AntigravityAgentConfig `queryParam:"inline" union:"member"` + CodeMenderAgentConfig *CodeMenderAgentConfig `queryParam:"inline" union:"member"` + DeepResearchAgentConfig *DeepResearchAgentConfig `queryParam:"inline" union:"member"` + DynamicAgentConfig *DynamicAgentConfig `queryParam:"inline" union:"member"` + + Type CreateAgentInteractionAgentConfigType +} + +func CreateCreateAgentInteractionAgentConfigAntigravity(antigravity AntigravityAgentConfig) CreateAgentInteractionAgentConfig { + typ := CreateAgentInteractionAgentConfigTypeAntigravity + + return CreateAgentInteractionAgentConfig{ + AntigravityAgentConfig: &antigravity, + Type: typ, + } +} + +func CreateCreateAgentInteractionAgentConfigCodeMender(codeMender CodeMenderAgentConfig) CreateAgentInteractionAgentConfig { + typ := CreateAgentInteractionAgentConfigTypeCodeMender + + return CreateAgentInteractionAgentConfig{ + CodeMenderAgentConfig: &codeMender, + Type: typ, + } +} + +func CreateCreateAgentInteractionAgentConfigDeepResearch(deepResearch DeepResearchAgentConfig) CreateAgentInteractionAgentConfig { + typ := CreateAgentInteractionAgentConfigTypeDeepResearch + + return CreateAgentInteractionAgentConfig{ + DeepResearchAgentConfig: &deepResearch, + Type: typ, + } +} + +func CreateCreateAgentInteractionAgentConfigDynamic(dynamic DynamicAgentConfig) CreateAgentInteractionAgentConfig { + typ := CreateAgentInteractionAgentConfigTypeDynamic + + return CreateAgentInteractionAgentConfig{ + DynamicAgentConfig: &dynamic, + Type: typ, + } +} + +func (u *CreateAgentInteractionAgentConfig) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateAgentInteractionAgentConfig{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + return fmt.Errorf("could not unmarshal discriminator: %w", err) + } + + switch dis.Type { + case "antigravity": + antigravityAgentConfig := new(AntigravityAgentConfig) + if err := utils.UnmarshalJSON(data, &antigravityAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == antigravity) type AntigravityAgentConfig within CreateAgentInteractionAgentConfig: %w", string(data), err) + } + + u.AntigravityAgentConfig = antigravityAgentConfig + u.Type = CreateAgentInteractionAgentConfigTypeAntigravity + return nil + case "code-mender": + codeMenderAgentConfig := new(CodeMenderAgentConfig) + if err := utils.UnmarshalJSON(data, &codeMenderAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code-mender) type CodeMenderAgentConfig within CreateAgentInteractionAgentConfig: %w", string(data), err) + } + + u.CodeMenderAgentConfig = codeMenderAgentConfig + u.Type = CreateAgentInteractionAgentConfigTypeCodeMender + return nil + case "deep-research": + deepResearchAgentConfig := new(DeepResearchAgentConfig) + if err := utils.UnmarshalJSON(data, &deepResearchAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == deep-research) type DeepResearchAgentConfig within CreateAgentInteractionAgentConfig: %w", string(data), err) + } + + u.DeepResearchAgentConfig = deepResearchAgentConfig + u.Type = CreateAgentInteractionAgentConfigTypeDeepResearch + return nil + case "dynamic": + dynamicAgentConfig := new(DynamicAgentConfig) + if err := utils.UnmarshalJSON(data, &dynamicAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == dynamic) type DynamicAgentConfig within CreateAgentInteractionAgentConfig: %w", string(data), err) + } + + u.DynamicAgentConfig = dynamicAgentConfig + u.Type = CreateAgentInteractionAgentConfigTypeDynamic + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionAgentConfig", string(data)) +} + +func (u CreateAgentInteractionAgentConfig) MarshalJSON() ([]byte, error) { + if u.AntigravityAgentConfig != nil { + return utils.MarshalJSON(u.AntigravityAgentConfig, "", true) + } + + if u.CodeMenderAgentConfig != nil { + return utils.MarshalJSON(u.CodeMenderAgentConfig, "", true) + } + + if u.DeepResearchAgentConfig != nil { + return utils.MarshalJSON(u.DeepResearchAgentConfig, "", true) + } + + if u.DynamicAgentConfig != nil { + return utils.MarshalJSON(u.DynamicAgentConfig, "", true) + } + + return nil, errors.New("could not marshal union type CreateAgentInteractionAgentConfig: all fields are null") +} + +type CreateAgentInteractionEnvironmentType string + +const ( + CreateAgentInteractionEnvironmentTypeEnvironment CreateAgentInteractionEnvironmentType = "Environment" + CreateAgentInteractionEnvironmentTypeStr CreateAgentInteractionEnvironmentType = "str" +) + +// CreateAgentInteractionEnvironment - The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. +type CreateAgentInteractionEnvironment struct { + Environment *Environment `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + + Type CreateAgentInteractionEnvironmentType +} + +func CreateCreateAgentInteractionEnvironmentEnvironment(environment Environment) CreateAgentInteractionEnvironment { + typ := CreateAgentInteractionEnvironmentTypeEnvironment + + return CreateAgentInteractionEnvironment{ + Environment: &environment, + Type: typ, + } +} + +func CreateCreateAgentInteractionEnvironmentStr(str string) CreateAgentInteractionEnvironment { + typ := CreateAgentInteractionEnvironmentTypeStr + + return CreateAgentInteractionEnvironment{ + Str: &str, + Type: typ, + } +} + +func (u *CreateAgentInteractionEnvironment) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateAgentInteractionEnvironment{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environment Environment = Environment{} + if err := utils.UnmarshalJSON(data, &environment, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateAgentInteractionEnvironmentTypeEnvironment, + Value: &environment, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateAgentInteractionEnvironmentTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionEnvironment", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionEnvironment", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(CreateAgentInteractionEnvironmentType) + switch best.Type { + case CreateAgentInteractionEnvironmentTypeEnvironment: + u.Environment = best.Value.(*Environment) + return nil + case CreateAgentInteractionEnvironmentTypeStr: + u.Str = best.Value.(*string) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionEnvironment", string(data)) +} + +func (u CreateAgentInteractionEnvironment) MarshalJSON() ([]byte, error) { + if u.Environment != nil { + return utils.MarshalJSON(u.Environment, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + return nil, errors.New("could not marshal union type CreateAgentInteractionEnvironment: all fields are null") +} + +type CreateAgentInteractionResponseFormatType string + +const ( + CreateAgentInteractionResponseFormatTypeResponseFormat CreateAgentInteractionResponseFormatType = "ResponseFormat" + CreateAgentInteractionResponseFormatTypeArrayOfResponseFormat CreateAgentInteractionResponseFormatType = "arrayOfResponseFormat" +) + +// CreateAgentInteractionResponseFormat - Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. +type CreateAgentInteractionResponseFormat struct { + ResponseFormat *ResponseFormat `queryParam:"inline" union:"member"` + ArrayOfResponseFormat []ResponseFormat `queryParam:"inline" union:"member"` + + Type CreateAgentInteractionResponseFormatType +} + +func CreateCreateAgentInteractionResponseFormatResponseFormat(responseFormat ResponseFormat) CreateAgentInteractionResponseFormat { + typ := CreateAgentInteractionResponseFormatTypeResponseFormat + + return CreateAgentInteractionResponseFormat{ + ResponseFormat: &responseFormat, + Type: typ, + } +} + +func CreateCreateAgentInteractionResponseFormatArrayOfResponseFormat(arrayOfResponseFormat []ResponseFormat) CreateAgentInteractionResponseFormat { + typ := CreateAgentInteractionResponseFormatTypeArrayOfResponseFormat + + return CreateAgentInteractionResponseFormat{ + ArrayOfResponseFormat: arrayOfResponseFormat, + Type: typ, + } +} + +func (u *CreateAgentInteractionResponseFormat) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateAgentInteractionResponseFormat{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var responseFormat ResponseFormat = ResponseFormat{} + if err := utils.UnmarshalJSON(data, &responseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateAgentInteractionResponseFormatTypeResponseFormat, + Value: &responseFormat, + }) + } + + var arrayOfResponseFormat []ResponseFormat = []ResponseFormat{} + if err := utils.UnmarshalJSON(data, &arrayOfResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateAgentInteractionResponseFormatTypeArrayOfResponseFormat, + Value: arrayOfResponseFormat, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionResponseFormat", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionResponseFormat", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(CreateAgentInteractionResponseFormatType) + switch best.Type { + case CreateAgentInteractionResponseFormatTypeResponseFormat: + u.ResponseFormat = best.Value.(*ResponseFormat) + return nil + case CreateAgentInteractionResponseFormatTypeArrayOfResponseFormat: + u.ArrayOfResponseFormat = best.Value.([]ResponseFormat) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateAgentInteractionResponseFormat", string(data)) +} + +func (u CreateAgentInteractionResponseFormat) MarshalJSON() ([]byte, error) { + if u.ResponseFormat != nil { + return utils.MarshalJSON(u.ResponseFormat, "", true) + } + + if u.ArrayOfResponseFormat != nil { + return utils.MarshalJSON(u.ArrayOfResponseFormat, "", true) + } + + return nil, errors.New("could not marshal union type CreateAgentInteractionResponseFormat: all fields are null") +} + +// CreateAgentInteraction - Parameters for creating agent interactions +type CreateAgentInteraction struct { + // The agent to interact with. + Agent AgentOption `json:"agent"` + // Configuration parameters for the agent interaction. + AgentConfig *CreateAgentInteractionAgentConfig `json:"agent_config,omitzero"` + // Input only. Whether to run the model interaction in the background. + Background *bool `json:"background,omitzero"` + // The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + Environment *CreateAgentInteractionEnvironment `json:"environment,omitzero"` + // The input for the interaction. + Input InteractionsInput `json:"input"` + // The labels with user-defined metadata for the request. + Labels map[string]string `json:"labels,omitzero"` + // The ID of the previous interaction, if any. + PreviousInteractionID *string `json:"previous_interaction_id,omitzero"` + // Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + ResponseFormat *CreateAgentInteractionResponseFormat `json:"response_format,omitzero"` + // The mime type of the response. This is required if response_format is set. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ResponseMimeType *string `json:"response_mime_type,omitzero"` + // The requested modalities of the response (TEXT, IMAGE, AUDIO). + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ResponseModalities []ResponseModality `json:"response_modalities,omitzero"` + // Safety settings for the interaction. + SafetySettings []SafetySetting `json:"safety_settings,omitzero"` + ServiceTier *ServiceTier `json:"service_tier,omitzero"` + // Input only. Whether to store the response and request for later retrieval. + Store *bool `json:"store,omitzero"` + // Input only. Whether the interaction is streamed as server-sent events. Defaults to true; set false to receive one complete interaction. + Stream *bool `default:"true" json:"stream"` + // System instruction for the interaction. + SystemInstruction *string `json:"system_instruction,omitzero"` + // A list of tool declarations the model may call during interaction. + Tools []Tool `json:"tools,omitzero"` + // Message for configuring webhook events for a request. + WebhookConfig *WebhookConfig `json:"webhook_config,omitzero"` +} + +func (c CreateAgentInteraction) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateAgentInteraction) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateAgentInteraction) GetAgent() AgentOption { + if c == nil { + return AgentOption("") + } + return c.Agent +} + +func (c *CreateAgentInteraction) GetAgentConfig() *CreateAgentInteractionAgentConfig { + if c == nil { + return nil + } + return c.AgentConfig +} + +func (c *CreateAgentInteraction) GetAgentConfigAntigravity() *AntigravityAgentConfig { + if v := c.GetAgentConfig(); v != nil { + return v.AntigravityAgentConfig + } + return nil +} + +func (c *CreateAgentInteraction) GetAgentConfigCodeMender() *CodeMenderAgentConfig { + if v := c.GetAgentConfig(); v != nil { + return v.CodeMenderAgentConfig + } + return nil +} + +func (c *CreateAgentInteraction) GetAgentConfigDeepResearch() *DeepResearchAgentConfig { + if v := c.GetAgentConfig(); v != nil { + return v.DeepResearchAgentConfig + } + return nil +} + +func (c *CreateAgentInteraction) GetAgentConfigDynamic() *DynamicAgentConfig { + if v := c.GetAgentConfig(); v != nil { + return v.DynamicAgentConfig + } + return nil +} + +func (c *CreateAgentInteraction) GetBackground() *bool { + if c == nil { + return nil + } + return c.Background +} + +func (c *CreateAgentInteraction) GetEnvironment() *CreateAgentInteractionEnvironment { + if c == nil { + return nil + } + return c.Environment +} + +func (c *CreateAgentInteraction) GetInput() InteractionsInput { + if c == nil { + return InteractionsInput{} + } + return c.Input +} + +func (c *CreateAgentInteraction) GetLabels() map[string]string { + if c == nil { + return nil + } + return c.Labels +} + +func (c *CreateAgentInteraction) GetPreviousInteractionID() *string { + if c == nil { + return nil + } + return c.PreviousInteractionID +} + +func (c *CreateAgentInteraction) GetResponseFormat() *CreateAgentInteractionResponseFormat { + if c == nil { + return nil + } + return c.ResponseFormat +} + +func (c *CreateAgentInteraction) GetResponseMimeType() *string { + if c == nil { + return nil + } + return c.ResponseMimeType +} + +func (c *CreateAgentInteraction) GetResponseModalities() []ResponseModality { + if c == nil { + return nil + } + return c.ResponseModalities +} + +func (c *CreateAgentInteraction) GetSafetySettings() []SafetySetting { + if c == nil { + return nil + } + return c.SafetySettings +} + +func (c *CreateAgentInteraction) GetServiceTier() *ServiceTier { + if c == nil { + return nil + } + return c.ServiceTier +} + +func (c *CreateAgentInteraction) GetStore() *bool { + if c == nil { + return nil + } + return c.Store +} + +func (c *CreateAgentInteraction) GetStream() *bool { + if c == nil { + return nil + } + return c.Stream +} + +func (c *CreateAgentInteraction) GetSystemInstruction() *string { + if c == nil { + return nil + } + return c.SystemInstruction +} + +func (c *CreateAgentInteraction) GetTools() []Tool { + if c == nil { + return nil + } + return c.Tools +} + +func (c *CreateAgentInteraction) GetWebhookConfig() *WebhookConfig { + if c == nil { + return nil + } + return c.WebhookConfig +} diff --git a/internal/sdk/models/interactions/createmodelinteraction.go b/internal/sdk/models/interactions/createmodelinteraction.go new file mode 100644 index 0000000..8429d16 --- /dev/null +++ b/internal/sdk/models/interactions/createmodelinteraction.go @@ -0,0 +1,389 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateModelInteractionEnvironmentType string + +const ( + CreateModelInteractionEnvironmentTypeEnvironment CreateModelInteractionEnvironmentType = "Environment" + CreateModelInteractionEnvironmentTypeStr CreateModelInteractionEnvironmentType = "str" +) + +// CreateModelInteractionEnvironment - The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. +type CreateModelInteractionEnvironment struct { + Environment *Environment `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + + Type CreateModelInteractionEnvironmentType +} + +func CreateCreateModelInteractionEnvironmentEnvironment(environment Environment) CreateModelInteractionEnvironment { + typ := CreateModelInteractionEnvironmentTypeEnvironment + + return CreateModelInteractionEnvironment{ + Environment: &environment, + Type: typ, + } +} + +func CreateCreateModelInteractionEnvironmentStr(str string) CreateModelInteractionEnvironment { + typ := CreateModelInteractionEnvironmentTypeStr + + return CreateModelInteractionEnvironment{ + Str: &str, + Type: typ, + } +} + +func (u *CreateModelInteractionEnvironment) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateModelInteractionEnvironment{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environment Environment = Environment{} + if err := utils.UnmarshalJSON(data, &environment, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateModelInteractionEnvironmentTypeEnvironment, + Value: &environment, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateModelInteractionEnvironmentTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateModelInteractionEnvironment", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateModelInteractionEnvironment", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(CreateModelInteractionEnvironmentType) + switch best.Type { + case CreateModelInteractionEnvironmentTypeEnvironment: + u.Environment = best.Value.(*Environment) + return nil + case CreateModelInteractionEnvironmentTypeStr: + u.Str = best.Value.(*string) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateModelInteractionEnvironment", string(data)) +} + +func (u CreateModelInteractionEnvironment) MarshalJSON() ([]byte, error) { + if u.Environment != nil { + return utils.MarshalJSON(u.Environment, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + return nil, errors.New("could not marshal union type CreateModelInteractionEnvironment: all fields are null") +} + +type CreateModelInteractionResponseFormatType string + +const ( + CreateModelInteractionResponseFormatTypeResponseFormat CreateModelInteractionResponseFormatType = "ResponseFormat" + CreateModelInteractionResponseFormatTypeArrayOfResponseFormat CreateModelInteractionResponseFormatType = "arrayOfResponseFormat" +) + +// CreateModelInteractionResponseFormat - Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. +type CreateModelInteractionResponseFormat struct { + ResponseFormat *ResponseFormat `queryParam:"inline" union:"member"` + ArrayOfResponseFormat []ResponseFormat `queryParam:"inline" union:"member"` + + Type CreateModelInteractionResponseFormatType +} + +func CreateCreateModelInteractionResponseFormatResponseFormat(responseFormat ResponseFormat) CreateModelInteractionResponseFormat { + typ := CreateModelInteractionResponseFormatTypeResponseFormat + + return CreateModelInteractionResponseFormat{ + ResponseFormat: &responseFormat, + Type: typ, + } +} + +func CreateCreateModelInteractionResponseFormatArrayOfResponseFormat(arrayOfResponseFormat []ResponseFormat) CreateModelInteractionResponseFormat { + typ := CreateModelInteractionResponseFormatTypeArrayOfResponseFormat + + return CreateModelInteractionResponseFormat{ + ArrayOfResponseFormat: arrayOfResponseFormat, + Type: typ, + } +} + +func (u *CreateModelInteractionResponseFormat) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateModelInteractionResponseFormat{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var responseFormat ResponseFormat = ResponseFormat{} + if err := utils.UnmarshalJSON(data, &responseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateModelInteractionResponseFormatTypeResponseFormat, + Value: &responseFormat, + }) + } + + var arrayOfResponseFormat []ResponseFormat = []ResponseFormat{} + if err := utils.UnmarshalJSON(data, &arrayOfResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateModelInteractionResponseFormatTypeArrayOfResponseFormat, + Value: arrayOfResponseFormat, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateModelInteractionResponseFormat", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateModelInteractionResponseFormat", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(CreateModelInteractionResponseFormatType) + switch best.Type { + case CreateModelInteractionResponseFormatTypeResponseFormat: + u.ResponseFormat = best.Value.(*ResponseFormat) + return nil + case CreateModelInteractionResponseFormatTypeArrayOfResponseFormat: + u.ArrayOfResponseFormat = best.Value.([]ResponseFormat) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateModelInteractionResponseFormat", string(data)) +} + +func (u CreateModelInteractionResponseFormat) MarshalJSON() ([]byte, error) { + if u.ResponseFormat != nil { + return utils.MarshalJSON(u.ResponseFormat, "", true) + } + + if u.ArrayOfResponseFormat != nil { + return utils.MarshalJSON(u.ArrayOfResponseFormat, "", true) + } + + return nil, errors.New("could not marshal union type CreateModelInteractionResponseFormat: all fields are null") +} + +// CreateModelInteraction - Parameters for creating model interactions +type CreateModelInteraction struct { + // Input only. Whether to run the model interaction in the background. + Background *bool `json:"background,omitzero"` + // The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + Environment *CreateModelInteractionEnvironment `json:"environment,omitzero"` + // Configuration parameters for model interactions. + GenerationConfig *GenerationConfig `json:"generation_config,omitzero"` + // The input for the interaction. + Input InteractionsInput `json:"input"` + // The labels with user-defined metadata for the request. + Labels map[string]string `json:"labels,omitzero"` + // The model that will complete your prompt.\n\nSee [models](https://ai.google.dev/gemini-api/docs/models) for additional details. + Model *Model `default:"gemini-3.6-flash" json:"model"` + // The ID of the previous interaction, if any. + PreviousInteractionID *string `json:"previous_interaction_id,omitzero"` + // Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + ResponseFormat *CreateModelInteractionResponseFormat `json:"response_format,omitzero"` + // The mime type of the response. This is required if response_format is set. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ResponseMimeType *string `json:"response_mime_type,omitzero"` + // The requested modalities of the response (TEXT, IMAGE, AUDIO). + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ResponseModalities []ResponseModality `json:"response_modalities,omitzero"` + // Safety settings for the interaction. + SafetySettings []SafetySetting `json:"safety_settings,omitzero"` + ServiceTier *ServiceTier `json:"service_tier,omitzero"` + // Input only. Whether to store the response and request for later retrieval. + Store *bool `json:"store,omitzero"` + // Input only. Whether the interaction is streamed as server-sent events. Defaults to true; set false to receive one complete interaction. + Stream *bool `default:"true" json:"stream"` + // System instruction for the interaction. + SystemInstruction *string `json:"system_instruction,omitzero"` + // A list of tool declarations the model may call during interaction. + Tools []Tool `json:"tools,omitzero"` + // Message for configuring webhook events for a request. + WebhookConfig *WebhookConfig `json:"webhook_config,omitzero"` +} + +func (c CreateModelInteraction) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateModelInteraction) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateModelInteraction) GetBackground() *bool { + if c == nil { + return nil + } + return c.Background +} + +func (c *CreateModelInteraction) GetEnvironment() *CreateModelInteractionEnvironment { + if c == nil { + return nil + } + return c.Environment +} + +func (c *CreateModelInteraction) GetGenerationConfig() *GenerationConfig { + if c == nil { + return nil + } + return c.GenerationConfig +} + +func (c *CreateModelInteraction) GetInput() InteractionsInput { + if c == nil { + return InteractionsInput{} + } + return c.Input +} + +func (c *CreateModelInteraction) GetLabels() map[string]string { + if c == nil { + return nil + } + return c.Labels +} + +func (c *CreateModelInteraction) GetModel() *Model { + if c == nil { + return nil + } + return c.Model +} + +func (c *CreateModelInteraction) GetPreviousInteractionID() *string { + if c == nil { + return nil + } + return c.PreviousInteractionID +} + +func (c *CreateModelInteraction) GetResponseFormat() *CreateModelInteractionResponseFormat { + if c == nil { + return nil + } + return c.ResponseFormat +} + +func (c *CreateModelInteraction) GetResponseMimeType() *string { + if c == nil { + return nil + } + return c.ResponseMimeType +} + +func (c *CreateModelInteraction) GetResponseModalities() []ResponseModality { + if c == nil { + return nil + } + return c.ResponseModalities +} + +func (c *CreateModelInteraction) GetSafetySettings() []SafetySetting { + if c == nil { + return nil + } + return c.SafetySettings +} + +func (c *CreateModelInteraction) GetServiceTier() *ServiceTier { + if c == nil { + return nil + } + return c.ServiceTier +} + +func (c *CreateModelInteraction) GetStore() *bool { + if c == nil { + return nil + } + return c.Store +} + +func (c *CreateModelInteraction) GetStream() *bool { + if c == nil { + return nil + } + return c.Stream +} + +func (c *CreateModelInteraction) GetSystemInstruction() *string { + if c == nil { + return nil + } + return c.SystemInstruction +} + +func (c *CreateModelInteraction) GetTools() []Tool { + if c == nil { + return nil + } + return c.Tools +} + +func (c *CreateModelInteraction) GetWebhookConfig() *WebhookConfig { + if c == nil { + return nil + } + return c.WebhookConfig +} diff --git a/internal/sdk/models/interactions/deepresearchagentconfig.go b/internal/sdk/models/interactions/deepresearchagentconfig.go new file mode 100644 index 0000000..7ded1a7 --- /dev/null +++ b/internal/sdk/models/interactions/deepresearchagentconfig.go @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Visualization - Whether to include visualizations in the response. +type Visualization string + +const ( + VisualizationOff Visualization = "off" + VisualizationAuto Visualization = "auto" +) + +func (e Visualization) ToPointer() *Visualization { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Visualization) IsExact() bool { + if e != nil { + switch *e { + case "off", "auto": + return true + } + } + return false +} + +// DeepResearchAgentConfig - Configuration for the Deep Research agent. +type DeepResearchAgentConfig struct { + // Enables human-in-the-loop planning for the Deep Research agent. If set to + // true, the Deep Research agent will provide a research plan in its response. + // The agent will then proceed only if the user confirms the plan in the next + // turn. + CollaborativePlanning *bool `json:"collaborative_planning,omitzero"` + // Enables bigquery tool for the Deep Research agent. + EnableBigqueryTool *bool `json:"enable_bigquery_tool,omitzero"` + ThinkingSummaries *ThinkingSummaries `json:"thinking_summaries,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"deep-research" json:"type"` + // Whether to include visualizations in the response. + Visualization *Visualization `json:"visualization,omitzero"` +} + +func (d DeepResearchAgentConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeepResearchAgentConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeepResearchAgentConfig) GetCollaborativePlanning() *bool { + if d == nil { + return nil + } + return d.CollaborativePlanning +} + +func (d *DeepResearchAgentConfig) GetEnableBigqueryTool() *bool { + if d == nil { + return nil + } + return d.EnableBigqueryTool +} + +func (d *DeepResearchAgentConfig) GetThinkingSummaries() *ThinkingSummaries { + if d == nil { + return nil + } + return d.ThinkingSummaries +} + +func (d *DeepResearchAgentConfig) GetType() string { + return "deep-research" +} + +func (d *DeepResearchAgentConfig) GetVisualization() *Visualization { + if d == nil { + return nil + } + return d.Visualization +} diff --git a/internal/sdk/models/interactions/documentcontent.go b/internal/sdk/models/interactions/documentcontent.go new file mode 100644 index 0000000..0378e01 --- /dev/null +++ b/internal/sdk/models/interactions/documentcontent.go @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// DocumentContentMimeType - The mime type of the document. +type DocumentContentMimeType string + +const ( + DocumentContentMimeTypeApplicationPdf DocumentContentMimeType = "application/pdf" + DocumentContentMimeTypeTextCsv DocumentContentMimeType = "text/csv" +) + +func (e DocumentContentMimeType) ToPointer() *DocumentContentMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *DocumentContentMimeType) IsExact() bool { + if e != nil { + switch *e { + case "application/pdf", "text/csv": + return true + } + } + return false +} + +// DocumentContent - A document content block. +type DocumentContent struct { + // The document content. + Data *string `json:"data,omitzero"` + // The mime type of the document. + MimeType *DocumentContentMimeType `json:"mime_type,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"document" json:"type"` + // The URI of the document. + URI *string `json:"uri,omitzero"` +} + +func (d DocumentContent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DocumentContent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DocumentContent) GetData() *string { + if d == nil { + return nil + } + return d.Data +} + +func (d *DocumentContent) GetMimeType() *DocumentContentMimeType { + if d == nil { + return nil + } + return d.MimeType +} + +func (d *DocumentContent) GetType() string { + return "document" +} + +func (d *DocumentContent) GetURI() *string { + if d == nil { + return nil + } + return d.URI +} diff --git a/internal/sdk/models/interactions/documentdelta.go b/internal/sdk/models/interactions/documentdelta.go new file mode 100644 index 0000000..7153fbe --- /dev/null +++ b/internal/sdk/models/interactions/documentdelta.go @@ -0,0 +1,87 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DocumentDeltaMimeType string + +const ( + DocumentDeltaMimeTypeApplicationPdf DocumentDeltaMimeType = "application/pdf" + DocumentDeltaMimeTypeTextCsv DocumentDeltaMimeType = "text/csv" +) + +func (e DocumentDeltaMimeType) ToPointer() *DocumentDeltaMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *DocumentDeltaMimeType) IsExact() bool { + if e != nil { + switch *e { + case "application/pdf", "text/csv": + return true + } + } + return false +} + +type DocumentDelta struct { + Data *string `json:"data,omitzero"` + MimeType *DocumentDeltaMimeType `json:"mime_type,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"document" json:"type"` + URI *string `json:"uri,omitzero"` +} + +func (d DocumentDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DocumentDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DocumentDelta) GetData() *string { + if d == nil { + return nil + } + return d.Data +} + +func (d *DocumentDelta) GetMimeType() *DocumentDeltaMimeType { + if d == nil { + return nil + } + return d.MimeType +} + +func (d *DocumentDelta) GetType() string { + return "document" +} + +func (d *DocumentDelta) GetURI() *string { + if d == nil { + return nil + } + return d.URI +} diff --git a/internal/sdk/models/interactions/dynamicagentconfig.go b/internal/sdk/models/interactions/dynamicagentconfig.go new file mode 100644 index 0000000..4e1f115 --- /dev/null +++ b/internal/sdk/models/interactions/dynamicagentconfig.go @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// DynamicAgentConfig - Configuration for dynamic agents. +type DynamicAgentConfig struct { + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"dynamic" json:"type"` + AdditionalProperties map[string]any `additionalProperties:"true" json:"-"` +} + +func (d DynamicAgentConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DynamicAgentConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DynamicAgentConfig) GetType() string { + return "dynamic" +} + +func (d *DynamicAgentConfig) GetAdditionalProperties() map[string]any { + if d == nil { + return nil + } + return d.AdditionalProperties +} diff --git a/internal/sdk/models/interactions/empty.go b/internal/sdk/models/interactions/empty.go new file mode 100644 index 0000000..d315f64 --- /dev/null +++ b/internal/sdk/models/interactions/empty.go @@ -0,0 +1,27 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +// Empty - A generic empty message that you can re-use to avoid defining duplicated +// empty messages in your APIs. A typical example is to use it as the request +// or the response type of an API method. For instance: +// +// service Foo { +// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); +// } +type Empty struct { +} diff --git a/internal/sdk/models/interactions/environment.go b/internal/sdk/models/interactions/environment.go new file mode 100644 index 0000000..dbed406 --- /dev/null +++ b/internal/sdk/models/interactions/environment.go @@ -0,0 +1,339 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type EnvType string + +const ( + EnvTypeEnvVar EnvType = "EnvVar" + EnvTypeMapOfEnvVar EnvType = "mapOfEnvVar" + EnvTypeUnknown EnvType = "Unknown" +) + +// Env - Environment variables to set in the sandbox environment. +type Env struct { + EnvVar *EnvVar `queryParam:"inline" union:"member"` + MapOfEnvVar map[string]EnvVar `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type EnvType +} + +func CreateEnvEnvVar(envVar EnvVar) Env { + typ := EnvTypeEnvVar + + return Env{ + EnvVar: &envVar, + Type: typ, + } +} + +func CreateEnvMapOfEnvVar(mapOfEnvVar map[string]EnvVar) Env { + typ := EnvTypeMapOfEnvVar + + return Env{ + MapOfEnvVar: mapOfEnvVar, + Type: typ, + } +} + +func CreateEnvUnknown(raw json.RawMessage) Env { + return Env{ + UnknownRaw: raw, + Type: EnvTypeUnknown, + } +} + +func (u Env) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Env) IsUnknown() bool { + return u.Type == EnvTypeUnknown +} + +func (u *Env) UnmarshalJSON(data []byte) error { + *u = Env{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var envVar EnvVar = EnvVar{} + if err := utils.UnmarshalJSON(data, &envVar, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvTypeEnvVar, + Value: &envVar, + }) + } + + var mapOfEnvVar map[string]EnvVar = map[string]EnvVar{} + if err := utils.UnmarshalJSON(data, &mapOfEnvVar, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvTypeMapOfEnvVar, + Value: mapOfEnvVar, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(EnvType) + switch best.Type { + case EnvTypeEnvVar: + u.EnvVar = best.Value.(*EnvVar) + return nil + case EnvTypeMapOfEnvVar: + u.MapOfEnvVar = best.Value.(map[string]EnvVar) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvTypeUnknown + return nil +} + +func (u Env) MarshalJSON() ([]byte, error) { + if u.EnvVar != nil { + return utils.MarshalJSON(u.EnvVar, "", true) + } + + if u.MapOfEnvVar != nil { + return utils.MarshalJSON(u.MapOfEnvVar, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Env: all fields are null") +} + +type NetworkEnum string + +const ( + NetworkEnumDisabled NetworkEnum = "disabled" +) + +func (e NetworkEnum) ToPointer() *NetworkEnum { + return &e +} +func (e *NetworkEnum) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "disabled": + *e = NetworkEnum(v) + return nil + default: + return fmt.Errorf("invalid value for NetworkEnum: %v", v) + } +} + +type NetworkType string + +const ( + NetworkTypeEnvironmentNetworkEgressAllowlist NetworkType = "EnvironmentNetworkEgressAllowlist" + NetworkTypeNetworkEnum NetworkType = "network_enum" + NetworkTypeUnknown NetworkType = "Unknown" +) + +// Network configuration for the environment. +type Network struct { + EnvironmentNetworkEgressAllowlist *EnvironmentNetworkEgressAllowlist `queryParam:"inline" union:"member"` + NetworkEnum *NetworkEnum `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type NetworkType +} + +func CreateNetworkEnvironmentNetworkEgressAllowlist(environmentNetworkEgressAllowlist EnvironmentNetworkEgressAllowlist) Network { + typ := NetworkTypeEnvironmentNetworkEgressAllowlist + + return Network{ + EnvironmentNetworkEgressAllowlist: &environmentNetworkEgressAllowlist, + Type: typ, + } +} + +func CreateNetworkNetworkEnum(networkEnum NetworkEnum) Network { + typ := NetworkTypeNetworkEnum + + return Network{ + NetworkEnum: &networkEnum, + Type: typ, + } +} + +func CreateNetworkUnknown(raw json.RawMessage) Network { + return Network{ + UnknownRaw: raw, + Type: NetworkTypeUnknown, + } +} + +func (u Network) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Network) IsUnknown() bool { + return u.Type == NetworkTypeUnknown +} + +func (u *Network) UnmarshalJSON(data []byte) error { + *u = Network{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environmentNetworkEgressAllowlist EnvironmentNetworkEgressAllowlist = EnvironmentNetworkEgressAllowlist{} + if err := utils.UnmarshalJSON(data, &environmentNetworkEgressAllowlist, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: NetworkTypeEnvironmentNetworkEgressAllowlist, + Value: &environmentNetworkEgressAllowlist, + }) + } + + var networkEnum NetworkEnum = NetworkEnum("") + if err := utils.UnmarshalJSON(data, &networkEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: NetworkTypeNetworkEnum, + Value: &networkEnum, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = NetworkTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = NetworkTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(NetworkType) + switch best.Type { + case NetworkTypeEnvironmentNetworkEgressAllowlist: + u.EnvironmentNetworkEgressAllowlist = best.Value.(*EnvironmentNetworkEgressAllowlist) + return nil + case NetworkTypeNetworkEnum: + u.NetworkEnum = best.Value.(*NetworkEnum) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = NetworkTypeUnknown + return nil +} + +func (u Network) MarshalJSON() ([]byte, error) { + if u.EnvironmentNetworkEgressAllowlist != nil { + return utils.MarshalJSON(u.EnvironmentNetworkEgressAllowlist, "", true) + } + + if u.NetworkEnum != nil { + return utils.MarshalJSON(u.NetworkEnum, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Network: all fields are null") +} + +// Environment - Configuration for a custom environment. +type Environment struct { + // Environment variables to set in the sandbox environment. + Env *Env `json:"env,omitzero"` + // Optional. The environment ID for the interaction. If specified, the request will + // update the existing environment instead of creating a new one. + EnvironmentID *string `json:"environment_id,omitzero"` + // Network configuration for the environment. + Network *Network `json:"network,omitzero"` + Sources []Source `json:"sources,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"remote" json:"type"` +} + +func (e Environment) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *Environment) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *Environment) GetEnv() *Env { + if e == nil { + return nil + } + return e.Env +} + +func (e *Environment) GetEnvironmentID() *string { + if e == nil { + return nil + } + return e.EnvironmentID +} + +func (e *Environment) GetNetwork() *Network { + if e == nil { + return nil + } + return e.Network +} + +func (e *Environment) GetSources() []Source { + if e == nil { + return nil + } + return e.Sources +} + +func (e *Environment) GetType() string { + return "remote" +} diff --git a/internal/sdk/models/interactions/environmentnetworkegressallowlist.go b/internal/sdk/models/interactions/environmentnetworkegressallowlist.go new file mode 100644 index 0000000..83631b2 --- /dev/null +++ b/internal/sdk/models/interactions/environmentnetworkegressallowlist.go @@ -0,0 +1,190 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Disabled - Turns all network off. +type Disabled string + +const ( + DisabledDisabled Disabled = "disabled" +) + +func (e Disabled) ToPointer() *Disabled { + return &e +} +func (e *Disabled) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "disabled": + *e = Disabled(v) + return nil + default: + return fmt.Errorf("invalid value for Disabled: %v", v) + } +} + +// Allowlist - Outbound networking configuration for the sandbox. When specified, restricts which external domains the sandbox can reach. Omit entirely to allow all outbound traffic with no header injection. +type Allowlist struct { + // List of allowed outbound domains. Only requests to listed domains are permitted. Use [{'domain': '*'}] to allow all domains while still injecting headers on specific ones. + Allowlist []AllowlistEntry `json:"allowlist,omitzero"` +} + +func (a Allowlist) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(a, "", false) +} + +func (a *Allowlist) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &a, "", false, nil); err != nil { + return err + } + return nil +} + +func (a *Allowlist) GetAllowlist() []AllowlistEntry { + if a == nil { + return nil + } + return a.Allowlist +} + +type EnvironmentNetworkEgressAllowlistType string + +const ( + EnvironmentNetworkEgressAllowlistTypeAllowlist EnvironmentNetworkEgressAllowlistType = "Allowlist" + EnvironmentNetworkEgressAllowlistTypeDisabled EnvironmentNetworkEgressAllowlistType = "Disabled" + EnvironmentNetworkEgressAllowlistTypeUnknown EnvironmentNetworkEgressAllowlistType = "Unknown" +) + +// EnvironmentNetworkEgressAllowlist - Outbound networking configuration for the sandbox. Accepts an object with an 'allowlist' array to restrict traffic, or the string 'disabled' to turn off all network access. Omit entirely to allow all outbound traffic with no header injection. +type EnvironmentNetworkEgressAllowlist struct { + Allowlist *Allowlist `queryParam:"inline" union:"member"` + Disabled *Disabled `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type EnvironmentNetworkEgressAllowlistType +} + +func CreateEnvironmentNetworkEgressAllowlistAllowlist(allowlist Allowlist) EnvironmentNetworkEgressAllowlist { + typ := EnvironmentNetworkEgressAllowlistTypeAllowlist + + return EnvironmentNetworkEgressAllowlist{ + Allowlist: &allowlist, + Type: typ, + } +} + +func CreateEnvironmentNetworkEgressAllowlistDisabled(disabled Disabled) EnvironmentNetworkEgressAllowlist { + typ := EnvironmentNetworkEgressAllowlistTypeDisabled + + return EnvironmentNetworkEgressAllowlist{ + Disabled: &disabled, + Type: typ, + } +} + +func CreateEnvironmentNetworkEgressAllowlistUnknown(raw json.RawMessage) EnvironmentNetworkEgressAllowlist { + return EnvironmentNetworkEgressAllowlist{ + UnknownRaw: raw, + Type: EnvironmentNetworkEgressAllowlistTypeUnknown, + } +} + +func (u EnvironmentNetworkEgressAllowlist) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u EnvironmentNetworkEgressAllowlist) IsUnknown() bool { + return u.Type == EnvironmentNetworkEgressAllowlistTypeUnknown +} + +func (u *EnvironmentNetworkEgressAllowlist) UnmarshalJSON(data []byte) error { + *u = EnvironmentNetworkEgressAllowlist{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var allowlist Allowlist = Allowlist{} + if err := utils.UnmarshalJSON(data, &allowlist, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentNetworkEgressAllowlistTypeAllowlist, + Value: &allowlist, + }) + } + + var disabled Disabled = Disabled("") + if err := utils.UnmarshalJSON(data, &disabled, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: EnvironmentNetworkEgressAllowlistTypeDisabled, + Value: &disabled, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvironmentNetworkEgressAllowlistTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvironmentNetworkEgressAllowlistTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(EnvironmentNetworkEgressAllowlistType) + switch best.Type { + case EnvironmentNetworkEgressAllowlistTypeAllowlist: + u.Allowlist = best.Value.(*Allowlist) + return nil + case EnvironmentNetworkEgressAllowlistTypeDisabled: + u.Disabled = best.Value.(*Disabled) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = EnvironmentNetworkEgressAllowlistTypeUnknown + return nil +} + +func (u EnvironmentNetworkEgressAllowlist) MarshalJSON() ([]byte, error) { + if u.Allowlist != nil { + return utils.MarshalJSON(u.Allowlist, "", true) + } + + if u.Disabled != nil { + return utils.MarshalJSON(u.Disabled, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type EnvironmentNetworkEgressAllowlist: all fields are null") +} diff --git a/internal/sdk/models/interactions/envvar.go b/internal/sdk/models/interactions/envvar.go new file mode 100644 index 0000000..5af3bf3 --- /dev/null +++ b/internal/sdk/models/interactions/envvar.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// EnvVar - An environment variable to set in the execution environment. +type EnvVar struct { + // Optional reference to a server-managed Credential resource by ID. + Credential *string `json:"credential,omitzero"` + // Direct string value for plain environment variables. + Value *string `json:"value,omitzero"` +} + +func (e EnvVar) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *EnvVar) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *EnvVar) GetCredential() *string { + if e == nil { + return nil + } + return e.Credential +} + +func (e *EnvVar) GetValue() *string { + if e == nil { + return nil + } + return e.Value +} diff --git a/internal/sdk/models/interactions/error.go b/internal/sdk/models/interactions/error.go new file mode 100644 index 0000000..0f11341 --- /dev/null +++ b/internal/sdk/models/interactions/error.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Error message from an interaction. +type Error struct { + // A URI that identifies the error type. + Code *string `json:"code,omitzero"` + // A human-readable error message. + Message *string `json:"message,omitzero"` +} + +func (e Error) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *Error) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *Error) GetCode() *string { + if e == nil { + return nil + } + return e.Code +} + +func (e *Error) GetMessage() *string { + if e == nil { + return nil + } + return e.Message +} diff --git a/internal/sdk/models/interactions/errorevent.go b/internal/sdk/models/interactions/errorevent.go new file mode 100644 index 0000000..2666e30 --- /dev/null +++ b/internal/sdk/models/interactions/errorevent.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ErrorEvent struct { + // Error message from an interaction. + Error *Error `json:"error,omitzero"` + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"error" json:"event_type"` +} + +func (e ErrorEvent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *ErrorEvent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *ErrorEvent) GetError() *Error { + if e == nil { + return nil + } + return e.Error +} + +func (e *ErrorEvent) GetEventID() *string { + if e == nil { + return nil + } + return e.EventID +} + +func (e *ErrorEvent) GetEventType() string { + return "error" +} diff --git a/internal/sdk/models/interactions/exaaisearchconfig.go b/internal/sdk/models/interactions/exaaisearchconfig.go new file mode 100644 index 0000000..f2c9f9a --- /dev/null +++ b/internal/sdk/models/interactions/exaaisearchconfig.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ExaAISearchConfig - Used to specify configuration for ExaAISearch. +type ExaAISearchConfig struct { + // Required. The API key for ExaAiSearch. + APIKey string `json:"api_key"` + // Optional. This field can be used to pass any parameter from the Exa.ai Search API. + CustomConfig map[string]any `json:"custom_config,omitzero"` +} + +func (e ExaAISearchConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(e, "", false) +} + +func (e *ExaAISearchConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &e, "", false, nil); err != nil { + return err + } + return nil +} + +func (e *ExaAISearchConfig) GetAPIKey() string { + if e == nil { + return "" + } + return e.APIKey +} + +func (e *ExaAISearchConfig) GetCustomConfig() map[string]any { + if e == nil { + return nil + } + return e.CustomConfig +} diff --git a/internal/sdk/models/interactions/filecitation.go b/internal/sdk/models/interactions/filecitation.go new file mode 100644 index 0000000..2de8b29 --- /dev/null +++ b/internal/sdk/models/interactions/filecitation.go @@ -0,0 +1,116 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FileCitation - A file citation annotation. +type FileCitation struct { + // User provided metadata about the retrieved context. + CustomMetadata map[string]any `json:"custom_metadata,omitzero"` + // The URI of the file. + DocumentURI *string `json:"document_uri,omitzero"` + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // The name of the file. + FileName *string `json:"file_name,omitzero"` + // Media ID in-case of image citations, if applicable. + MediaID *string `json:"media_id,omitzero"` + // Page number of the cited document, if applicable. + PageNumber *int `json:"page_number,omitzero"` + // Source attributed for a portion of the text. + Source *string `json:"source,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"file_citation" json:"type"` +} + +func (f FileCitation) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileCitation) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileCitation) GetCustomMetadata() map[string]any { + if f == nil { + return nil + } + return f.CustomMetadata +} + +func (f *FileCitation) GetDocumentURI() *string { + if f == nil { + return nil + } + return f.DocumentURI +} + +func (f *FileCitation) GetEndIndex() *int { + if f == nil { + return nil + } + return f.EndIndex +} + +func (f *FileCitation) GetFileName() *string { + if f == nil { + return nil + } + return f.FileName +} + +func (f *FileCitation) GetMediaID() *string { + if f == nil { + return nil + } + return f.MediaID +} + +func (f *FileCitation) GetPageNumber() *int { + if f == nil { + return nil + } + return f.PageNumber +} + +func (f *FileCitation) GetSource() *string { + if f == nil { + return nil + } + return f.Source +} + +func (f *FileCitation) GetStartIndex() *int { + if f == nil { + return nil + } + return f.StartIndex +} + +func (f *FileCitation) GetType() string { + return "file_citation" +} diff --git a/internal/sdk/models/interactions/filecontent.go b/internal/sdk/models/interactions/filecontent.go new file mode 100644 index 0000000..2bb6ba6 --- /dev/null +++ b/internal/sdk/models/interactions/filecontent.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FileContent - Content of a single file in the codebase. +type FileContent struct { + // The UTF-8 encoded text content of the file. + Content *string `json:"content,omitzero"` + // The relative path of the file from the project root. + Path *string `json:"path,omitzero"` +} + +func (f FileContent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileContent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileContent) GetContent() *string { + if f == nil { + return nil + } + return f.Content +} + +func (f *FileContent) GetPath() *string { + if f == nil { + return nil + } + return f.Path +} diff --git a/internal/sdk/models/interactions/filesearch.go b/internal/sdk/models/interactions/filesearch.go new file mode 100644 index 0000000..d99f515 --- /dev/null +++ b/internal/sdk/models/interactions/filesearch.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FileSearch - A tool that can be used by the model to search files. +type FileSearch struct { + // The file search store names to search. + FileSearchStoreNames []string `json:"file_search_store_names,omitzero"` + // Metadata filter to apply to the semantic retrieval documents and chunks. + MetadataFilter *string `json:"metadata_filter,omitzero"` + // The number of semantic retrieval chunks to retrieve. + TopK *int `json:"top_k,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"file_search" json:"type"` +} + +func (f FileSearch) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileSearch) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileSearch) GetFileSearchStoreNames() []string { + if f == nil { + return nil + } + return f.FileSearchStoreNames +} + +func (f *FileSearch) GetMetadataFilter() *string { + if f == nil { + return nil + } + return f.MetadataFilter +} + +func (f *FileSearch) GetTopK() *int { + if f == nil { + return nil + } + return f.TopK +} + +func (f *FileSearch) GetType() string { + return "file_search" +} diff --git a/internal/sdk/models/interactions/filesearchcalldelta.go b/internal/sdk/models/interactions/filesearchcalldelta.go new file mode 100644 index 0000000..2481a79 --- /dev/null +++ b/internal/sdk/models/interactions/filesearchcalldelta.go @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FileSearchCallDelta struct { + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"file_search_call" json:"type"` +} + +func (f FileSearchCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileSearchCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileSearchCallDelta) GetSignature() *string { + if f == nil { + return nil + } + return f.Signature +} + +func (f *FileSearchCallDelta) GetType() string { + return "file_search_call" +} diff --git a/internal/sdk/models/interactions/filesearchcallstep.go b/internal/sdk/models/interactions/filesearchcallstep.go new file mode 100644 index 0000000..f7558cf --- /dev/null +++ b/internal/sdk/models/interactions/filesearchcallstep.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FileSearchCallStep - File Search call step. +type FileSearchCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"file_search_call" json:"type"` +} + +func (f FileSearchCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileSearchCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileSearchCallStep) GetID() string { + if f == nil { + return "" + } + return f.ID +} + +func (f *FileSearchCallStep) GetSignature() *string { + if f == nil { + return nil + } + return f.Signature +} + +func (f *FileSearchCallStep) GetType() string { + return "file_search_call" +} diff --git a/internal/sdk/models/interactions/filesearchresult.go b/internal/sdk/models/interactions/filesearchresult.go new file mode 100644 index 0000000..8e9a0ac --- /dev/null +++ b/internal/sdk/models/interactions/filesearchresult.go @@ -0,0 +1,36 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FileSearchResult - The result of the File Search. +type FileSearchResult struct { +} + +func (f FileSearchResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileSearchResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} diff --git a/internal/sdk/models/interactions/filesearchresultdelta.go b/internal/sdk/models/interactions/filesearchresultdelta.go new file mode 100644 index 0000000..85cf7d6 --- /dev/null +++ b/internal/sdk/models/interactions/filesearchresultdelta.go @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FileSearchResultDelta struct { + Result []FileSearchResult `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"file_search_result" json:"type"` +} + +func (f FileSearchResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileSearchResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileSearchResultDelta) GetResult() []FileSearchResult { + if f == nil { + return []FileSearchResult{} + } + return f.Result +} + +func (f *FileSearchResultDelta) GetSignature() *string { + if f == nil { + return nil + } + return f.Signature +} + +func (f *FileSearchResultDelta) GetType() string { + return "file_search_result" +} diff --git a/internal/sdk/models/interactions/filesearchresultstep.go b/internal/sdk/models/interactions/filesearchresultstep.go new file mode 100644 index 0000000..a374feb --- /dev/null +++ b/internal/sdk/models/interactions/filesearchresultstep.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FileSearchResultStep - File Search result step. +type FileSearchResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"file_search_result" json:"type"` +} + +func (f FileSearchResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FileSearchResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FileSearchResultStep) GetCallID() string { + if f == nil { + return "" + } + return f.CallID +} + +func (f *FileSearchResultStep) GetSignature() *string { + if f == nil { + return nil + } + return f.Signature +} + +func (f *FileSearchResultStep) GetType() string { + return "file_search_result" +} diff --git a/internal/sdk/models/interactions/filter.go b/internal/sdk/models/interactions/filter.go new file mode 100644 index 0000000..4947654 --- /dev/null +++ b/internal/sdk/models/interactions/filter.go @@ -0,0 +1,65 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Filter - Config for filters. +type Filter struct { + // Optional. String for metadata filtering. + MetadataFilter *string `json:"metadata_filter,omitzero"` + // Optional. Only returns contexts with vector distance smaller than the + // threshold. + VectorDistanceThreshold *float64 `json:"vector_distance_threshold,omitzero"` + // Optional. Only returns contexts with vector similarity larger than the + // threshold. + VectorSimilarityThreshold *float64 `json:"vector_similarity_threshold,omitzero"` +} + +func (f Filter) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *Filter) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *Filter) GetMetadataFilter() *string { + if f == nil { + return nil + } + return f.MetadataFilter +} + +func (f *Filter) GetVectorDistanceThreshold() *float64 { + if f == nil { + return nil + } + return f.VectorDistanceThreshold +} + +func (f *Filter) GetVectorSimilarityThreshold() *float64 { + if f == nil { + return nil + } + return f.VectorSimilarityThreshold +} diff --git a/internal/sdk/models/interactions/findrequest.go b/internal/sdk/models/interactions/findrequest.go new file mode 100644 index 0000000..b1e4a9b --- /dev/null +++ b/internal/sdk/models/interactions/findrequest.go @@ -0,0 +1,99 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Mode - The mode of the find session. +type Mode string + +const ( + ModeScan Mode = "scan" + ModeVerify Mode = "verify" +) + +func (e Mode) ToPointer() *Mode { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Mode) IsExact() bool { + if e != nil { + switch *e { + case "scan", "verify": + return true + } + } + return false +} + +// FindRequest - Request parameters specific to FIND sessions, used for discovering +// vulnerabilities in a codebase. +type FindRequest struct { + // Additional context or custom instructions provided by the user to guide + // the vulnerability analysis. + Description *string `json:"description,omitzero"` + // The identifier of a specific finding to verify. This is primarily used in + // VERIFY mode to focus the agent's execution-based validation on a single + // vulnerability. + FindingID *string `json:"finding_id,omitzero"` + // The mode of the find session. + Mode *Mode `json:"mode,omitzero"` + // A list of source files to provide as context for the scan. + SourceFiles []FileContent `json:"source_files,omitzero"` +} + +func (f FindRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FindRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FindRequest) GetDescription() *string { + if f == nil { + return nil + } + return f.Description +} + +func (f *FindRequest) GetFindingID() *string { + if f == nil { + return nil + } + return f.FindingID +} + +func (f *FindRequest) GetMode() *Mode { + if f == nil { + return nil + } + return f.Mode +} + +func (f *FindRequest) GetSourceFiles() []FileContent { + if f == nil { + return nil + } + return f.SourceFiles +} diff --git a/internal/sdk/models/interactions/fixrequest.go b/internal/sdk/models/interactions/fixrequest.go new file mode 100644 index 0000000..638d0ca --- /dev/null +++ b/internal/sdk/models/interactions/fixrequest.go @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FixRequest - Request parameters specific to FIX sessions, used for generating and +// validating security patches. +type FixRequest struct { + // Additional context or custom instructions provided by the user to guide + // the patch generation process. + Description *string `json:"description,omitzero"` + // The identifier of the specific security finding to be remediated. This ID + // maps to a previously discovered vulnerability. + FindingID *string `json:"finding_id,omitzero"` + // A list of source files providing context for the remediation. These files + // are typically the ones containing the identified vulnerability. + SourceFiles []FileContent `json:"source_files,omitzero"` +} + +func (f FixRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FixRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FixRequest) GetDescription() *string { + if f == nil { + return nil + } + return f.Description +} + +func (f *FixRequest) GetFindingID() *string { + if f == nil { + return nil + } + return f.FindingID +} + +func (f *FixRequest) GetSourceFiles() []FileContent { + if f == nil { + return nil + } + return f.SourceFiles +} diff --git a/internal/sdk/models/interactions/function.go b/internal/sdk/models/interactions/function.go new file mode 100644 index 0000000..26b5167 --- /dev/null +++ b/internal/sdk/models/interactions/function.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Function - A tool that can be used by the model. +type Function struct { + // A description of the function. + Description *string `json:"description,omitzero"` + // The name of the function. + Name *string `json:"name,omitzero"` + // The JSON Schema for the function's parameters. + Parameters any `json:"parameters,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"function" json:"type"` +} + +func (f Function) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *Function) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *Function) GetDescription() *string { + if f == nil { + return nil + } + return f.Description +} + +func (f *Function) GetName() *string { + if f == nil { + return nil + } + return f.Name +} + +func (f *Function) GetParameters() any { + if f == nil { + return nil + } + return f.Parameters +} + +func (f *Function) GetType() string { + return "function" +} diff --git a/internal/sdk/models/interactions/functioncallstep.go b/internal/sdk/models/interactions/functioncallstep.go new file mode 100644 index 0000000..1cd7e96 --- /dev/null +++ b/internal/sdk/models/interactions/functioncallstep.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// FunctionCallStep - A function tool call step. +type FunctionCallStep struct { + // Required. The arguments to pass to the function. + Arguments map[string]any `json:"arguments"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // Required. The name of the tool to call. + Name string `json:"name"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"function_call" json:"type"` +} + +func (f FunctionCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FunctionCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FunctionCallStep) GetArguments() map[string]any { + if f == nil { + return map[string]any{} + } + return f.Arguments +} + +func (f *FunctionCallStep) GetID() string { + if f == nil { + return "" + } + return f.ID +} + +func (f *FunctionCallStep) GetName() string { + if f == nil { + return "" + } + return f.Name +} + +func (f *FunctionCallStep) GetType() string { + return "function_call" +} diff --git a/internal/sdk/models/interactions/functionresultdelta.go b/internal/sdk/models/interactions/functionresultdelta.go new file mode 100644 index 0000000..4bf29f0 --- /dev/null +++ b/internal/sdk/models/interactions/functionresultdelta.go @@ -0,0 +1,224 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FunctionResultDeltaResult struct { +} + +func (f FunctionResultDeltaResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FunctionResultDeltaResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +type FunctionResultDeltaResultUnionType string + +const ( + FunctionResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent FunctionResultDeltaResultUnionType = "arrayOfFunctionResultSubcontent" + FunctionResultDeltaResultUnionTypeFunctionResultDeltaResult FunctionResultDeltaResultUnionType = "FunctionResultDelta_result" + FunctionResultDeltaResultUnionTypeStr FunctionResultDeltaResultUnionType = "str" + FunctionResultDeltaResultUnionTypeUnknown FunctionResultDeltaResultUnionType = "Unknown" +) + +type FunctionResultDeltaResultUnion struct { + ArrayOfFunctionResultSubcontent []FunctionResultSubcontent `queryParam:"inline" union:"member"` + FunctionResultDeltaResult *FunctionResultDeltaResult `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type FunctionResultDeltaResultUnionType +} + +func CreateFunctionResultDeltaResultUnionArrayOfFunctionResultSubcontent(arrayOfFunctionResultSubcontent []FunctionResultSubcontent) FunctionResultDeltaResultUnion { + typ := FunctionResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent + + return FunctionResultDeltaResultUnion{ + ArrayOfFunctionResultSubcontent: arrayOfFunctionResultSubcontent, + Type: typ, + } +} + +func CreateFunctionResultDeltaResultUnionFunctionResultDeltaResult(functionResultDeltaResult FunctionResultDeltaResult) FunctionResultDeltaResultUnion { + typ := FunctionResultDeltaResultUnionTypeFunctionResultDeltaResult + + return FunctionResultDeltaResultUnion{ + FunctionResultDeltaResult: &functionResultDeltaResult, + Type: typ, + } +} + +func CreateFunctionResultDeltaResultUnionStr(str string) FunctionResultDeltaResultUnion { + typ := FunctionResultDeltaResultUnionTypeStr + + return FunctionResultDeltaResultUnion{ + Str: &str, + Type: typ, + } +} + +func CreateFunctionResultDeltaResultUnionUnknown(raw json.RawMessage) FunctionResultDeltaResultUnion { + return FunctionResultDeltaResultUnion{ + UnknownRaw: raw, + Type: FunctionResultDeltaResultUnionTypeUnknown, + } +} + +func (u FunctionResultDeltaResultUnion) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u FunctionResultDeltaResultUnion) IsUnknown() bool { + return u.Type == FunctionResultDeltaResultUnionTypeUnknown +} + +func (u *FunctionResultDeltaResultUnion) UnmarshalJSON(data []byte) error { + *u = FunctionResultDeltaResultUnion{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var arrayOfFunctionResultSubcontent []FunctionResultSubcontent = []FunctionResultSubcontent{} + if err := utils.UnmarshalJSON(data, &arrayOfFunctionResultSubcontent, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: FunctionResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent, + Value: arrayOfFunctionResultSubcontent, + }) + } + + var functionResultDeltaResult FunctionResultDeltaResult = FunctionResultDeltaResult{} + if err := utils.UnmarshalJSON(data, &functionResultDeltaResult, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: FunctionResultDeltaResultUnionTypeFunctionResultDeltaResult, + Value: &functionResultDeltaResult, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: FunctionResultDeltaResultUnionTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultDeltaResultUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultDeltaResultUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(FunctionResultDeltaResultUnionType) + switch best.Type { + case FunctionResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent: + u.ArrayOfFunctionResultSubcontent = best.Value.([]FunctionResultSubcontent) + return nil + case FunctionResultDeltaResultUnionTypeFunctionResultDeltaResult: + u.FunctionResultDeltaResult = best.Value.(*FunctionResultDeltaResult) + return nil + case FunctionResultDeltaResultUnionTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultDeltaResultUnionTypeUnknown + return nil +} + +func (u FunctionResultDeltaResultUnion) MarshalJSON() ([]byte, error) { + if u.ArrayOfFunctionResultSubcontent != nil { + return utils.MarshalJSON(u.ArrayOfFunctionResultSubcontent, "", true) + } + + if u.FunctionResultDeltaResult != nil { + return utils.MarshalJSON(u.FunctionResultDeltaResult, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type FunctionResultDeltaResultUnion: all fields are null") +} + +type FunctionResultDelta struct { + IsError *bool `json:"is_error,omitzero"` + Name *string `json:"name,omitzero"` + Result FunctionResultDeltaResultUnion `json:"result"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"function_result" json:"type"` +} + +func (f FunctionResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FunctionResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FunctionResultDelta) GetIsError() *bool { + if f == nil { + return nil + } + return f.IsError +} + +func (f *FunctionResultDelta) GetName() *string { + if f == nil { + return nil + } + return f.Name +} + +func (f *FunctionResultDelta) GetResult() FunctionResultDeltaResultUnion { + if f == nil { + return FunctionResultDeltaResultUnion{} + } + return f.Result +} + +func (f *FunctionResultDelta) GetType() string { + return "function_result" +} diff --git a/internal/sdk/models/interactions/functionresultstep.go b/internal/sdk/models/interactions/functionresultstep.go new file mode 100644 index 0000000..f16875a --- /dev/null +++ b/internal/sdk/models/interactions/functionresultstep.go @@ -0,0 +1,238 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FunctionResultStepResult struct { +} + +func (f FunctionResultStepResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FunctionResultStepResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +type FunctionResultStepResultUnionType string + +const ( + FunctionResultStepResultUnionTypeArrayOfFunctionResultSubcontent FunctionResultStepResultUnionType = "arrayOfFunctionResultSubcontent" + FunctionResultStepResultUnionTypeFunctionResultStepResult FunctionResultStepResultUnionType = "FunctionResultStep_result" + FunctionResultStepResultUnionTypeStr FunctionResultStepResultUnionType = "str" + FunctionResultStepResultUnionTypeUnknown FunctionResultStepResultUnionType = "Unknown" +) + +// FunctionResultStepResultUnion - Required. The result of the tool call. +type FunctionResultStepResultUnion struct { + ArrayOfFunctionResultSubcontent []FunctionResultSubcontent `queryParam:"inline" union:"member"` + FunctionResultStepResult *FunctionResultStepResult `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type FunctionResultStepResultUnionType +} + +func CreateFunctionResultStepResultUnionArrayOfFunctionResultSubcontent(arrayOfFunctionResultSubcontent []FunctionResultSubcontent) FunctionResultStepResultUnion { + typ := FunctionResultStepResultUnionTypeArrayOfFunctionResultSubcontent + + return FunctionResultStepResultUnion{ + ArrayOfFunctionResultSubcontent: arrayOfFunctionResultSubcontent, + Type: typ, + } +} + +func CreateFunctionResultStepResultUnionFunctionResultStepResult(functionResultStepResult FunctionResultStepResult) FunctionResultStepResultUnion { + typ := FunctionResultStepResultUnionTypeFunctionResultStepResult + + return FunctionResultStepResultUnion{ + FunctionResultStepResult: &functionResultStepResult, + Type: typ, + } +} + +func CreateFunctionResultStepResultUnionStr(str string) FunctionResultStepResultUnion { + typ := FunctionResultStepResultUnionTypeStr + + return FunctionResultStepResultUnion{ + Str: &str, + Type: typ, + } +} + +func CreateFunctionResultStepResultUnionUnknown(raw json.RawMessage) FunctionResultStepResultUnion { + return FunctionResultStepResultUnion{ + UnknownRaw: raw, + Type: FunctionResultStepResultUnionTypeUnknown, + } +} + +func (u FunctionResultStepResultUnion) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u FunctionResultStepResultUnion) IsUnknown() bool { + return u.Type == FunctionResultStepResultUnionTypeUnknown +} + +func (u *FunctionResultStepResultUnion) UnmarshalJSON(data []byte) error { + *u = FunctionResultStepResultUnion{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var arrayOfFunctionResultSubcontent []FunctionResultSubcontent = []FunctionResultSubcontent{} + if err := utils.UnmarshalJSON(data, &arrayOfFunctionResultSubcontent, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: FunctionResultStepResultUnionTypeArrayOfFunctionResultSubcontent, + Value: arrayOfFunctionResultSubcontent, + }) + } + + var functionResultStepResult FunctionResultStepResult = FunctionResultStepResult{} + if err := utils.UnmarshalJSON(data, &functionResultStepResult, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: FunctionResultStepResultUnionTypeFunctionResultStepResult, + Value: &functionResultStepResult, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: FunctionResultStepResultUnionTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultStepResultUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultStepResultUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(FunctionResultStepResultUnionType) + switch best.Type { + case FunctionResultStepResultUnionTypeArrayOfFunctionResultSubcontent: + u.ArrayOfFunctionResultSubcontent = best.Value.([]FunctionResultSubcontent) + return nil + case FunctionResultStepResultUnionTypeFunctionResultStepResult: + u.FunctionResultStepResult = best.Value.(*FunctionResultStepResult) + return nil + case FunctionResultStepResultUnionTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultStepResultUnionTypeUnknown + return nil +} + +func (u FunctionResultStepResultUnion) MarshalJSON() ([]byte, error) { + if u.ArrayOfFunctionResultSubcontent != nil { + return utils.MarshalJSON(u.ArrayOfFunctionResultSubcontent, "", true) + } + + if u.FunctionResultStepResult != nil { + return utils.MarshalJSON(u.FunctionResultStepResult, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type FunctionResultStepResultUnion: all fields are null") +} + +// FunctionResultStep - Result of a function tool call. +type FunctionResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // Whether the tool call resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // The name of the tool that was called. + Name *string `json:"name,omitzero"` + // Required. The result of the tool call. + Result FunctionResultStepResultUnion `json:"result"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"function_result" json:"type"` +} + +func (f FunctionResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FunctionResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FunctionResultStep) GetCallID() string { + if f == nil { + return "" + } + return f.CallID +} + +func (f *FunctionResultStep) GetIsError() *bool { + if f == nil { + return nil + } + return f.IsError +} + +func (f *FunctionResultStep) GetName() *string { + if f == nil { + return nil + } + return f.Name +} + +func (f *FunctionResultStep) GetResult() FunctionResultStepResultUnion { + if f == nil { + return FunctionResultStepResultUnion{} + } + return f.Result +} + +func (f *FunctionResultStep) GetType() string { + return "function_result" +} diff --git a/internal/sdk/models/interactions/functionresultsubcontent.go b/internal/sdk/models/interactions/functionresultsubcontent.go new file mode 100644 index 0000000..7936c01 --- /dev/null +++ b/internal/sdk/models/interactions/functionresultsubcontent.go @@ -0,0 +1,141 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FunctionResultSubcontentType string + +const ( + FunctionResultSubcontentTypeImage FunctionResultSubcontentType = "image" + FunctionResultSubcontentTypeText FunctionResultSubcontentType = "text" + FunctionResultSubcontentTypeUnknown FunctionResultSubcontentType = "UNKNOWN" +) + +type FunctionResultSubcontent struct { + ImageContent *ImageContent `queryParam:"inline" union:"member"` + TextContent *TextContent `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type FunctionResultSubcontentType +} + +func CreateFunctionResultSubcontentImage(image ImageContent) FunctionResultSubcontent { + typ := FunctionResultSubcontentTypeImage + + return FunctionResultSubcontent{ + ImageContent: &image, + Type: typ, + } +} + +func CreateFunctionResultSubcontentText(text TextContent) FunctionResultSubcontent { + typ := FunctionResultSubcontentTypeText + + return FunctionResultSubcontent{ + TextContent: &text, + Type: typ, + } +} + +func CreateFunctionResultSubcontentUnknown(raw json.RawMessage) FunctionResultSubcontent { + return FunctionResultSubcontent{ + UnknownRaw: raw, + Type: FunctionResultSubcontentTypeUnknown, + } +} + +func (u FunctionResultSubcontent) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u FunctionResultSubcontent) IsUnknown() bool { + return u.Type == FunctionResultSubcontentTypeUnknown +} + +func (u *FunctionResultSubcontent) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = FunctionResultSubcontent{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultSubcontentTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultSubcontentTypeUnknown + return nil + } + + switch dis.Type { + case "image": + imageContent := new(ImageContent) + if err := utils.UnmarshalJSON(data, &imageContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == image) type ImageContent within FunctionResultSubcontent: %w", string(data), err) + } + + u.ImageContent = imageContent + u.Type = FunctionResultSubcontentTypeImage + return nil + case "text": + textContent := new(TextContent) + if err := utils.UnmarshalJSON(data, &textContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == text) type TextContent within FunctionResultSubcontent: %w", string(data), err) + } + + u.TextContent = textContent + u.Type = FunctionResultSubcontentTypeText + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = FunctionResultSubcontentTypeUnknown + return nil + } + +} + +func (u FunctionResultSubcontent) MarshalJSON() ([]byte, error) { + if u.ImageContent != nil { + return utils.MarshalJSON(u.ImageContent, "", true) + } + + if u.TextContent != nil { + return utils.MarshalJSON(u.TextContent, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type FunctionResultSubcontent: all fields are null") +} diff --git a/internal/sdk/models/interactions/generationconfig.go b/internal/sdk/models/interactions/generationconfig.go new file mode 100644 index 0000000..9175d06 --- /dev/null +++ b/internal/sdk/models/interactions/generationconfig.go @@ -0,0 +1,363 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type SpeechConfigUnionType string + +const ( + SpeechConfigUnionTypeSpeakerConfig SpeechConfigUnionType = "SpeakerConfig" + SpeechConfigUnionTypeArrayOfSpeechConfig SpeechConfigUnionType = "arrayOfSpeechConfig" + SpeechConfigUnionTypeUnknown SpeechConfigUnionType = "Unknown" +) + +// SpeechConfigUnion - Optional. Speech and multi-speaker configuration. +type SpeechConfigUnion struct { + SpeakerConfig *SpeakerConfig `queryParam:"inline" union:"member"` + ArrayOfSpeechConfig []SpeechConfig `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type SpeechConfigUnionType +} + +func CreateSpeechConfigUnionSpeakerConfig(speakerConfig SpeakerConfig) SpeechConfigUnion { + typ := SpeechConfigUnionTypeSpeakerConfig + + return SpeechConfigUnion{ + SpeakerConfig: &speakerConfig, + Type: typ, + } +} + +func CreateSpeechConfigUnionArrayOfSpeechConfig(arrayOfSpeechConfig []SpeechConfig) SpeechConfigUnion { + typ := SpeechConfigUnionTypeArrayOfSpeechConfig + + return SpeechConfigUnion{ + ArrayOfSpeechConfig: arrayOfSpeechConfig, + Type: typ, + } +} + +func CreateSpeechConfigUnionUnknown(raw json.RawMessage) SpeechConfigUnion { + return SpeechConfigUnion{ + UnknownRaw: raw, + Type: SpeechConfigUnionTypeUnknown, + } +} + +func (u SpeechConfigUnion) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u SpeechConfigUnion) IsUnknown() bool { + return u.Type == SpeechConfigUnionTypeUnknown +} + +func (u *SpeechConfigUnion) UnmarshalJSON(data []byte) error { + *u = SpeechConfigUnion{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var speakerConfig SpeakerConfig = SpeakerConfig{} + if err := utils.UnmarshalJSON(data, &speakerConfig, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: SpeechConfigUnionTypeSpeakerConfig, + Value: &speakerConfig, + }) + } + + var arrayOfSpeechConfig []SpeechConfig = []SpeechConfig{} + if err := utils.UnmarshalJSON(data, &arrayOfSpeechConfig, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: SpeechConfigUnionTypeArrayOfSpeechConfig, + Value: arrayOfSpeechConfig, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = SpeechConfigUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = SpeechConfigUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(SpeechConfigUnionType) + switch best.Type { + case SpeechConfigUnionTypeSpeakerConfig: + u.SpeakerConfig = best.Value.(*SpeakerConfig) + return nil + case SpeechConfigUnionTypeArrayOfSpeechConfig: + u.ArrayOfSpeechConfig = best.Value.([]SpeechConfig) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = SpeechConfigUnionTypeUnknown + return nil +} + +func (u SpeechConfigUnion) MarshalJSON() ([]byte, error) { + if u.SpeakerConfig != nil { + return utils.MarshalJSON(u.SpeakerConfig, "", true) + } + + if u.ArrayOfSpeechConfig != nil { + return utils.MarshalJSON(u.ArrayOfSpeechConfig, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type SpeechConfigUnion: all fields are null") +} + +type ToolChoiceUnionType string + +const ( + ToolChoiceUnionTypeToolChoiceConfig ToolChoiceUnionType = "ToolChoiceConfig" + ToolChoiceUnionTypeToolChoiceType ToolChoiceUnionType = "ToolChoiceType" + ToolChoiceUnionTypeUnknown ToolChoiceUnionType = "Unknown" +) + +// ToolChoice - The tool choice configuration. +type ToolChoice struct { + ToolChoiceConfig *ToolChoiceConfig `queryParam:"inline" union:"member"` + ToolChoiceType *ToolChoiceType `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type ToolChoiceUnionType +} + +func CreateToolChoiceToolChoiceConfig(toolChoiceConfig ToolChoiceConfig) ToolChoice { + typ := ToolChoiceUnionTypeToolChoiceConfig + + return ToolChoice{ + ToolChoiceConfig: &toolChoiceConfig, + Type: typ, + } +} + +func CreateToolChoiceToolChoiceType(toolChoiceType ToolChoiceType) ToolChoice { + typ := ToolChoiceUnionTypeToolChoiceType + + return ToolChoice{ + ToolChoiceType: &toolChoiceType, + Type: typ, + } +} + +func CreateToolChoiceUnknown(raw json.RawMessage) ToolChoice { + return ToolChoice{ + UnknownRaw: raw, + Type: ToolChoiceUnionTypeUnknown, + } +} + +func (u ToolChoice) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u ToolChoice) IsUnknown() bool { + return u.Type == ToolChoiceUnionTypeUnknown +} + +func (u *ToolChoice) UnmarshalJSON(data []byte) error { + *u = ToolChoice{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var toolChoiceConfig ToolChoiceConfig = ToolChoiceConfig{} + if err := utils.UnmarshalJSON(data, &toolChoiceConfig, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ToolChoiceUnionTypeToolChoiceConfig, + Value: &toolChoiceConfig, + }) + } + + var toolChoiceType ToolChoiceType = ToolChoiceType("") + if err := utils.UnmarshalJSON(data, &toolChoiceType, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ToolChoiceUnionTypeToolChoiceType, + Value: &toolChoiceType, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = ToolChoiceUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ToolChoiceUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(ToolChoiceUnionType) + switch best.Type { + case ToolChoiceUnionTypeToolChoiceConfig: + u.ToolChoiceConfig = best.Value.(*ToolChoiceConfig) + return nil + case ToolChoiceUnionTypeToolChoiceType: + u.ToolChoiceType = best.Value.(*ToolChoiceType) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = ToolChoiceUnionTypeUnknown + return nil +} + +func (u ToolChoice) MarshalJSON() ([]byte, error) { + if u.ToolChoiceConfig != nil { + return utils.MarshalJSON(u.ToolChoiceConfig, "", true) + } + + if u.ToolChoiceType != nil { + return utils.MarshalJSON(u.ToolChoiceType, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type ToolChoice: all fields are null") +} + +// GenerationConfig - Configuration parameters for model interactions. +type GenerationConfig struct { + // The configuration for image interaction. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ImageConfig *ImageConfig `json:"image_config,omitzero"` + // The maximum number of tokens to include in the response. + MaxOutputTokens *int `json:"max_output_tokens,omitzero"` + // Seed used in decoding for reproducibility. + Seed *int `json:"seed,omitzero"` + // Optional. Speech and multi-speaker configuration. + SpeechConfig *SpeechConfigUnion `json:"speech_config,omitzero"` + // A list of character sequences that will stop output interaction. + StopSequences []string `json:"stop_sequences,omitzero"` + ThinkingLevel *ThinkingLevel `json:"thinking_level,omitzero"` + ThinkingSummaries *ThinkingSummaries `json:"thinking_summaries,omitzero"` + // The tool choice configuration. + ToolChoice *ToolChoice `json:"tool_choice,omitzero"` + // Configuration for speech recognition (transcription). + TranscriptionConfig *TranscriptionConfig `json:"transcription_config,omitzero"` + // Configuration options for video generation. + VideoConfig *VideoConfig `json:"video_config,omitzero"` +} + +func (g GenerationConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GenerationConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GenerationConfig) GetImageConfig() *ImageConfig { + if g == nil { + return nil + } + return g.ImageConfig +} + +func (g *GenerationConfig) GetMaxOutputTokens() *int { + if g == nil { + return nil + } + return g.MaxOutputTokens +} + +func (g *GenerationConfig) GetSeed() *int { + if g == nil { + return nil + } + return g.Seed +} + +func (g *GenerationConfig) GetSpeechConfig() *SpeechConfigUnion { + if g == nil { + return nil + } + return g.SpeechConfig +} + +func (g *GenerationConfig) GetStopSequences() []string { + if g == nil { + return nil + } + return g.StopSequences +} + +func (g *GenerationConfig) GetThinkingLevel() *ThinkingLevel { + if g == nil { + return nil + } + return g.ThinkingLevel +} + +func (g *GenerationConfig) GetThinkingSummaries() *ThinkingSummaries { + if g == nil { + return nil + } + return g.ThinkingSummaries +} + +func (g *GenerationConfig) GetToolChoice() *ToolChoice { + if g == nil { + return nil + } + return g.ToolChoice +} + +func (g *GenerationConfig) GetTranscriptionConfig() *TranscriptionConfig { + if g == nil { + return nil + } + return g.TranscriptionConfig +} + +func (g *GenerationConfig) GetVideoConfig() *VideoConfig { + if g == nil { + return nil + } + return g.VideoConfig +} diff --git a/internal/sdk/models/interactions/googlemaps.go b/internal/sdk/models/interactions/googlemaps.go new file mode 100644 index 0000000..4032373 --- /dev/null +++ b/internal/sdk/models/interactions/googlemaps.go @@ -0,0 +1,70 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleMaps - A tool that can be used by the model to call Google Maps. +type GoogleMaps struct { + // Whether to return a widget context token in the tool call result of the + // response. + EnableWidget *bool `json:"enable_widget,omitzero"` + // The latitude of the user's location. + Latitude *float64 `json:"latitude,omitzero"` + // The longitude of the user's location. + Longitude *float64 `json:"longitude,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_maps" json:"type"` +} + +func (g GoogleMaps) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMaps) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMaps) GetEnableWidget() *bool { + if g == nil { + return nil + } + return g.EnableWidget +} + +func (g *GoogleMaps) GetLatitude() *float64 { + if g == nil { + return nil + } + return g.Latitude +} + +func (g *GoogleMaps) GetLongitude() *float64 { + if g == nil { + return nil + } + return g.Longitude +} + +func (g *GoogleMaps) GetType() string { + return "google_maps" +} diff --git a/internal/sdk/models/interactions/googlemapscallarguments.go b/internal/sdk/models/interactions/googlemapscallarguments.go new file mode 100644 index 0000000..d751c0f --- /dev/null +++ b/internal/sdk/models/interactions/googlemapscallarguments.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleMapsCallArguments - The arguments to pass to the Google Maps tool. +type GoogleMapsCallArguments struct { + // The queries to be executed. + Queries []string `json:"queries,omitzero"` +} + +func (g GoogleMapsCallArguments) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsCallArguments) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsCallArguments) GetQueries() []string { + if g == nil { + return nil + } + return g.Queries +} diff --git a/internal/sdk/models/interactions/googlemapscalldelta.go b/internal/sdk/models/interactions/googlemapscalldelta.go new file mode 100644 index 0000000..c5b6d7f --- /dev/null +++ b/internal/sdk/models/interactions/googlemapscalldelta.go @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GoogleMapsCallDelta struct { + // The arguments to pass to the Google Maps tool. + Arguments *GoogleMapsCallArguments `json:"arguments,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_maps_call" json:"type"` +} + +func (g GoogleMapsCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsCallDelta) GetArguments() *GoogleMapsCallArguments { + if g == nil { + return nil + } + return g.Arguments +} + +func (g *GoogleMapsCallDelta) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleMapsCallDelta) GetType() string { + return "google_maps_call" +} diff --git a/internal/sdk/models/interactions/googlemapscallstep.go b/internal/sdk/models/interactions/googlemapscallstep.go new file mode 100644 index 0000000..d264f79 --- /dev/null +++ b/internal/sdk/models/interactions/googlemapscallstep.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleMapsCallStep - Google Maps call step. +type GoogleMapsCallStep struct { + // The arguments to pass to the Google Maps tool. + Arguments *GoogleMapsCallArguments `json:"arguments,omitzero"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_maps_call" json:"type"` +} + +func (g GoogleMapsCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsCallStep) GetArguments() *GoogleMapsCallArguments { + if g == nil { + return nil + } + return g.Arguments +} + +func (g *GoogleMapsCallStep) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +func (g *GoogleMapsCallStep) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleMapsCallStep) GetType() string { + return "google_maps_call" +} diff --git a/internal/sdk/models/interactions/googlemapsresult.go b/internal/sdk/models/interactions/googlemapsresult.go new file mode 100644 index 0000000..ea7e33c --- /dev/null +++ b/internal/sdk/models/interactions/googlemapsresult.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleMapsResult - The result of the Google Maps. +type GoogleMapsResult struct { + Places []GoogleMapsResultPlaces `json:"places,omitzero"` + WidgetContextToken *string `json:"widget_context_token,omitzero"` +} + +func (g GoogleMapsResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsResult) GetPlaces() []GoogleMapsResultPlaces { + if g == nil { + return nil + } + return g.Places +} + +func (g *GoogleMapsResult) GetWidgetContextToken() *string { + if g == nil { + return nil + } + return g.WidgetContextToken +} diff --git a/internal/sdk/models/interactions/googlemapsresultdelta.go b/internal/sdk/models/interactions/googlemapsresultdelta.go new file mode 100644 index 0000000..3b8f6fb --- /dev/null +++ b/internal/sdk/models/interactions/googlemapsresultdelta.go @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GoogleMapsResultDelta struct { + // The results of the Google Maps. + Result []GoogleMapsResult `json:"result,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_maps_result" json:"type"` +} + +func (g GoogleMapsResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsResultDelta) GetResult() []GoogleMapsResult { + if g == nil { + return nil + } + return g.Result +} + +func (g *GoogleMapsResultDelta) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleMapsResultDelta) GetType() string { + return "google_maps_result" +} diff --git a/internal/sdk/models/interactions/googlemapsresultplaces.go b/internal/sdk/models/interactions/googlemapsresultplaces.go new file mode 100644 index 0000000..1e9e452 --- /dev/null +++ b/internal/sdk/models/interactions/googlemapsresultplaces.go @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GoogleMapsResultPlaces struct { + Name *string `json:"name,omitzero"` + PlaceID *string `json:"place_id,omitzero"` + ReviewSnippets []ReviewSnippet `json:"review_snippets,omitzero"` + URL *string `json:"url,omitzero"` +} + +func (g GoogleMapsResultPlaces) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsResultPlaces) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsResultPlaces) GetName() *string { + if g == nil { + return nil + } + return g.Name +} + +func (g *GoogleMapsResultPlaces) GetPlaceID() *string { + if g == nil { + return nil + } + return g.PlaceID +} + +func (g *GoogleMapsResultPlaces) GetReviewSnippets() []ReviewSnippet { + if g == nil { + return nil + } + return g.ReviewSnippets +} + +func (g *GoogleMapsResultPlaces) GetURL() *string { + if g == nil { + return nil + } + return g.URL +} diff --git a/internal/sdk/models/interactions/googlemapsresultstep.go b/internal/sdk/models/interactions/googlemapsresultstep.go new file mode 100644 index 0000000..3aec49a --- /dev/null +++ b/internal/sdk/models/interactions/googlemapsresultstep.go @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleMapsResultStep - Google Maps result step. +type GoogleMapsResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + Result []GoogleMapsResult `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_maps_result" json:"type"` +} + +func (g GoogleMapsResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleMapsResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleMapsResultStep) GetCallID() string { + if g == nil { + return "" + } + return g.CallID +} + +func (g *GoogleMapsResultStep) GetResult() []GoogleMapsResult { + if g == nil { + return []GoogleMapsResult{} + } + return g.Result +} + +func (g *GoogleMapsResultStep) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleMapsResultStep) GetType() string { + return "google_maps_result" +} diff --git a/internal/sdk/models/interactions/googlesearch.go b/internal/sdk/models/interactions/googlesearch.go new file mode 100644 index 0000000..012fb10 --- /dev/null +++ b/internal/sdk/models/interactions/googlesearch.go @@ -0,0 +1,74 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GoogleSearchSearchType string + +const ( + GoogleSearchSearchTypeWebSearch GoogleSearchSearchType = "web_search" + GoogleSearchSearchTypeImageSearch GoogleSearchSearchType = "image_search" + GoogleSearchSearchTypeEnterpriseWebSearch GoogleSearchSearchType = "enterprise_web_search" +) + +func (e GoogleSearchSearchType) ToPointer() *GoogleSearchSearchType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *GoogleSearchSearchType) IsExact() bool { + if e != nil { + switch *e { + case "web_search", "image_search", "enterprise_web_search": + return true + } + } + return false +} + +// GoogleSearch - A tool that can be used by the model to search Google. +type GoogleSearch struct { + // The types of search grounding to enable. + SearchTypes []GoogleSearchSearchType `json:"search_types,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_search" json:"type"` +} + +func (g GoogleSearch) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearch) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearch) GetSearchTypes() []GoogleSearchSearchType { + if g == nil { + return nil + } + return g.SearchTypes +} + +func (g *GoogleSearch) GetType() string { + return "google_search" +} diff --git a/internal/sdk/models/interactions/googlesearchcallarguments.go b/internal/sdk/models/interactions/googlesearchcallarguments.go new file mode 100644 index 0000000..98461c4 --- /dev/null +++ b/internal/sdk/models/interactions/googlesearchcallarguments.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleSearchCallArguments - The arguments to pass to Google Search. +type GoogleSearchCallArguments struct { + // Web search queries for the following-up web search. + Queries []string `json:"queries,omitzero"` +} + +func (g GoogleSearchCallArguments) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearchCallArguments) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearchCallArguments) GetQueries() []string { + if g == nil { + return nil + } + return g.Queries +} diff --git a/internal/sdk/models/interactions/googlesearchcalldelta.go b/internal/sdk/models/interactions/googlesearchcalldelta.go new file mode 100644 index 0000000..d6585cd --- /dev/null +++ b/internal/sdk/models/interactions/googlesearchcalldelta.go @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GoogleSearchCallDelta struct { + // The arguments to pass to Google Search. + Arguments GoogleSearchCallArguments `json:"arguments"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_search_call" json:"type"` +} + +func (g GoogleSearchCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearchCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearchCallDelta) GetArguments() GoogleSearchCallArguments { + if g == nil { + return GoogleSearchCallArguments{} + } + return g.Arguments +} + +func (g *GoogleSearchCallDelta) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleSearchCallDelta) GetType() string { + return "google_search_call" +} diff --git a/internal/sdk/models/interactions/googlesearchcallstep.go b/internal/sdk/models/interactions/googlesearchcallstep.go new file mode 100644 index 0000000..e8aa4c5 --- /dev/null +++ b/internal/sdk/models/interactions/googlesearchcallstep.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleSearchCallStepSearchType - The type of search grounding enabled. +type GoogleSearchCallStepSearchType string + +const ( + GoogleSearchCallStepSearchTypeWebSearch GoogleSearchCallStepSearchType = "web_search" + GoogleSearchCallStepSearchTypeImageSearch GoogleSearchCallStepSearchType = "image_search" + GoogleSearchCallStepSearchTypeEnterpriseWebSearch GoogleSearchCallStepSearchType = "enterprise_web_search" +) + +func (e GoogleSearchCallStepSearchType) ToPointer() *GoogleSearchCallStepSearchType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *GoogleSearchCallStepSearchType) IsExact() bool { + if e != nil { + switch *e { + case "web_search", "image_search", "enterprise_web_search": + return true + } + } + return false +} + +// GoogleSearchCallStep - Google Search call step. +type GoogleSearchCallStep struct { + // The arguments to pass to Google Search. + Arguments GoogleSearchCallArguments `json:"arguments"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // The type of search grounding enabled. + SearchType *GoogleSearchCallStepSearchType `json:"search_type,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_search_call" json:"type"` +} + +func (g GoogleSearchCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearchCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearchCallStep) GetArguments() GoogleSearchCallArguments { + if g == nil { + return GoogleSearchCallArguments{} + } + return g.Arguments +} + +func (g *GoogleSearchCallStep) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +func (g *GoogleSearchCallStep) GetSearchType() *GoogleSearchCallStepSearchType { + if g == nil { + return nil + } + return g.SearchType +} + +func (g *GoogleSearchCallStep) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleSearchCallStep) GetType() string { + return "google_search_call" +} diff --git a/internal/sdk/models/interactions/googlesearchresult.go b/internal/sdk/models/interactions/googlesearchresult.go new file mode 100644 index 0000000..c6e54a8 --- /dev/null +++ b/internal/sdk/models/interactions/googlesearchresult.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleSearchResult - The result of the Google Search. +type GoogleSearchResult struct { + // Web content snippet that can be embedded in a web page or an app webview. + SearchSuggestions *string `json:"search_suggestions,omitzero"` +} + +func (g GoogleSearchResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearchResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearchResult) GetSearchSuggestions() *string { + if g == nil { + return nil + } + return g.SearchSuggestions +} diff --git a/internal/sdk/models/interactions/googlesearchresultdelta.go b/internal/sdk/models/interactions/googlesearchresultdelta.go new file mode 100644 index 0000000..bcdbc41 --- /dev/null +++ b/internal/sdk/models/interactions/googlesearchresultdelta.go @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GoogleSearchResultDelta struct { + IsError *bool `json:"is_error,omitzero"` + Result []GoogleSearchResult `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_search_result" json:"type"` +} + +func (g GoogleSearchResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearchResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearchResultDelta) GetIsError() *bool { + if g == nil { + return nil + } + return g.IsError +} + +func (g *GoogleSearchResultDelta) GetResult() []GoogleSearchResult { + if g == nil { + return []GoogleSearchResult{} + } + return g.Result +} + +func (g *GoogleSearchResultDelta) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleSearchResultDelta) GetType() string { + return "google_search_result" +} diff --git a/internal/sdk/models/interactions/googlesearchresultstep.go b/internal/sdk/models/interactions/googlesearchresultstep.go new file mode 100644 index 0000000..f891bec --- /dev/null +++ b/internal/sdk/models/interactions/googlesearchresultstep.go @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GoogleSearchResultStep - Google Search result step. +type GoogleSearchResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // Whether the Google Search resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // Required. The results of the Google Search. + Result []GoogleSearchResult `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"google_search_result" json:"type"` +} + +func (g GoogleSearchResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GoogleSearchResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GoogleSearchResultStep) GetCallID() string { + if g == nil { + return "" + } + return g.CallID +} + +func (g *GoogleSearchResultStep) GetIsError() *bool { + if g == nil { + return nil + } + return g.IsError +} + +func (g *GoogleSearchResultStep) GetResult() []GoogleSearchResult { + if g == nil { + return []GoogleSearchResult{} + } + return g.Result +} + +func (g *GoogleSearchResultStep) GetSignature() *string { + if g == nil { + return nil + } + return g.Signature +} + +func (g *GoogleSearchResultStep) GetType() string { + return "google_search_result" +} diff --git a/internal/sdk/models/interactions/groundingtoolcount.go b/internal/sdk/models/interactions/groundingtoolcount.go new file mode 100644 index 0000000..b3e6833 --- /dev/null +++ b/internal/sdk/models/interactions/groundingtoolcount.go @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// GroundingToolCountType - The grounding tool type associated with the count. +type GroundingToolCountType string + +const ( + GroundingToolCountTypeGoogleSearch GroundingToolCountType = "google_search" + GroundingToolCountTypeGoogleMaps GroundingToolCountType = "google_maps" + GroundingToolCountTypeRetrieval GroundingToolCountType = "retrieval" +) + +func (e GroundingToolCountType) ToPointer() *GroundingToolCountType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *GroundingToolCountType) IsExact() bool { + if e != nil { + switch *e { + case "google_search", "google_maps", "retrieval": + return true + } + } + return false +} + +// GroundingToolCount - The number of grounding tool counts. +type GroundingToolCount struct { + // The number of grounding tool counts. + Count *int `json:"count,omitzero"` + // The grounding tool type associated with the count. + Type *GroundingToolCountType `json:"type,omitzero"` +} + +func (g GroundingToolCount) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GroundingToolCount) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GroundingToolCount) GetCount() *int { + if g == nil { + return nil + } + return g.Count +} + +func (g *GroundingToolCount) GetType() *GroundingToolCountType { + if g == nil { + return nil + } + return g.Type +} diff --git a/internal/sdk/models/interactions/harmcategory.go b/internal/sdk/models/interactions/harmcategory.go new file mode 100644 index 0000000..20245ff --- /dev/null +++ b/internal/sdk/models/interactions/harmcategory.go @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type HarmCategory string + +const ( + HarmCategoryHateSpeech HarmCategory = "hate_speech" + HarmCategoryDangerousContent HarmCategory = "dangerous_content" + HarmCategoryHarassment HarmCategory = "harassment" + HarmCategorySexuallyExplicit HarmCategory = "sexually_explicit" + HarmCategoryCivicIntegrity HarmCategory = "civic_integrity" + HarmCategoryImageHate HarmCategory = "image_hate" + HarmCategoryImageDangerousContent HarmCategory = "image_dangerous_content" + HarmCategoryImageHarassment HarmCategory = "image_harassment" + HarmCategoryImageSexuallyExplicit HarmCategory = "image_sexually_explicit" + HarmCategoryJailbreak HarmCategory = "jailbreak" +) + +func (e HarmCategory) ToPointer() *HarmCategory { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *HarmCategory) IsExact() bool { + if e != nil { + switch *e { + case "hate_speech", "dangerous_content", "harassment", "sexually_explicit", "civic_integrity", "image_hate", "image_dangerous_content", "image_harassment", "image_sexually_explicit", "jailbreak": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/hybridsearch.go b/internal/sdk/models/interactions/hybridsearch.go new file mode 100644 index 0000000..6964f1d --- /dev/null +++ b/internal/sdk/models/interactions/hybridsearch.go @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// HybridSearch - Config for Hybrid Search. +type HybridSearch struct { + // Optional. Alpha value controls the weight between dense and sparse vector search + // results. + Alpha *float32 `json:"alpha,omitzero"` +} + +func (h HybridSearch) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(h, "", false) +} + +func (h *HybridSearch) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &h, "", false, nil); err != nil { + return err + } + return nil +} + +func (h *HybridSearch) GetAlpha() *float32 { + if h == nil { + return nil + } + return h.Alpha +} diff --git a/internal/sdk/models/interactions/imageconfig.go b/internal/sdk/models/interactions/imageconfig.go new file mode 100644 index 0000000..9e8d5b9 --- /dev/null +++ b/internal/sdk/models/interactions/imageconfig.go @@ -0,0 +1,112 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ImageConfigAspectRatio string + +const ( + ImageConfigAspectRatioEleven ImageConfigAspectRatio = "1:1" + ImageConfigAspectRatioTwentyThree ImageConfigAspectRatio = "2:3" + ImageConfigAspectRatioThirtyTwo ImageConfigAspectRatio = "3:2" + ImageConfigAspectRatioThirtyFour ImageConfigAspectRatio = "3:4" + ImageConfigAspectRatioFortyThree ImageConfigAspectRatio = "4:3" + ImageConfigAspectRatioFortyFive ImageConfigAspectRatio = "4:5" + ImageConfigAspectRatioFiftyFour ImageConfigAspectRatio = "5:4" + ImageConfigAspectRatioNineHundredAndSixteen ImageConfigAspectRatio = "9:16" + ImageConfigAspectRatioOneHundredAndSixtyNine ImageConfigAspectRatio = "16:9" + ImageConfigAspectRatioTwoHundredAndNineteen ImageConfigAspectRatio = "21:9" + ImageConfigAspectRatioEighteen ImageConfigAspectRatio = "1:8" + ImageConfigAspectRatioEightyOne ImageConfigAspectRatio = "8:1" + ImageConfigAspectRatioFourteen ImageConfigAspectRatio = "1:4" + ImageConfigAspectRatioFortyOne ImageConfigAspectRatio = "4:1" +) + +func (e ImageConfigAspectRatio) ToPointer() *ImageConfigAspectRatio { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageConfigAspectRatio) IsExact() bool { + if e != nil { + switch *e { + case "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1": + return true + } + } + return false +} + +type ImageConfigImageSize string + +const ( + ImageConfigImageSizeOneK ImageConfigImageSize = "1K" + ImageConfigImageSizeTwoK ImageConfigImageSize = "2K" + ImageConfigImageSizeFourK ImageConfigImageSize = "4K" + ImageConfigImageSizeFiveHundredAndTwelve ImageConfigImageSize = "512" +) + +func (e ImageConfigImageSize) ToPointer() *ImageConfigImageSize { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageConfigImageSize) IsExact() bool { + if e != nil { + switch *e { + case "1K", "2K", "4K", "512": + return true + } + } + return false +} + +// ImageConfig - The configuration for image interaction. +// +// Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. +type ImageConfig struct { + AspectRatio *ImageConfigAspectRatio `json:"aspect_ratio,omitzero"` + ImageSize *ImageConfigImageSize `json:"image_size,omitzero"` +} + +func (i ImageConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *ImageConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *ImageConfig) GetAspectRatio() *ImageConfigAspectRatio { + if i == nil { + return nil + } + return i.AspectRatio +} + +func (i *ImageConfig) GetImageSize() *ImageConfigImageSize { + if i == nil { + return nil + } + return i.ImageSize +} diff --git a/internal/sdk/models/interactions/imagecontent.go b/internal/sdk/models/interactions/imagecontent.go new file mode 100644 index 0000000..b10b469 --- /dev/null +++ b/internal/sdk/models/interactions/imagecontent.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ImageContentMimeType - The mime type of the image. +type ImageContentMimeType string + +const ( + ImageContentMimeTypeImagePng ImageContentMimeType = "image/png" + ImageContentMimeTypeImageJpeg ImageContentMimeType = "image/jpeg" + ImageContentMimeTypeImageWebp ImageContentMimeType = "image/webp" + ImageContentMimeTypeImageHeic ImageContentMimeType = "image/heic" + ImageContentMimeTypeImageHeif ImageContentMimeType = "image/heif" + ImageContentMimeTypeImageGif ImageContentMimeType = "image/gif" + ImageContentMimeTypeImageBmp ImageContentMimeType = "image/bmp" + ImageContentMimeTypeImageTiff ImageContentMimeType = "image/tiff" +) + +func (e ImageContentMimeType) ToPointer() *ImageContentMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageContentMimeType) IsExact() bool { + if e != nil { + switch *e { + case "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff": + return true + } + } + return false +} + +// ImageContent - An image content block. +type ImageContent struct { + // The image content. + Data *string `json:"data,omitzero"` + // The mime type of the image. + MimeType *ImageContentMimeType `json:"mime_type,omitzero"` + Resolution *MediaResolution `json:"resolution,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"image" json:"type"` + // The URI of the image. + URI *string `json:"uri,omitzero"` +} + +func (i ImageContent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *ImageContent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *ImageContent) GetData() *string { + if i == nil { + return nil + } + return i.Data +} + +func (i *ImageContent) GetMimeType() *ImageContentMimeType { + if i == nil { + return nil + } + return i.MimeType +} + +func (i *ImageContent) GetResolution() *MediaResolution { + if i == nil { + return nil + } + return i.Resolution +} + +func (i *ImageContent) GetType() string { + return "image" +} + +func (i *ImageContent) GetURI() *string { + if i == nil { + return nil + } + return i.URI +} diff --git a/internal/sdk/models/interactions/imagedelta.go b/internal/sdk/models/interactions/imagedelta.go new file mode 100644 index 0000000..4542e96 --- /dev/null +++ b/internal/sdk/models/interactions/imagedelta.go @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ImageDeltaMimeType string + +const ( + ImageDeltaMimeTypeImagePng ImageDeltaMimeType = "image/png" + ImageDeltaMimeTypeImageJpeg ImageDeltaMimeType = "image/jpeg" + ImageDeltaMimeTypeImageWebp ImageDeltaMimeType = "image/webp" + ImageDeltaMimeTypeImageHeic ImageDeltaMimeType = "image/heic" + ImageDeltaMimeTypeImageHeif ImageDeltaMimeType = "image/heif" + ImageDeltaMimeTypeImageGif ImageDeltaMimeType = "image/gif" + ImageDeltaMimeTypeImageBmp ImageDeltaMimeType = "image/bmp" + ImageDeltaMimeTypeImageTiff ImageDeltaMimeType = "image/tiff" +) + +func (e ImageDeltaMimeType) ToPointer() *ImageDeltaMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageDeltaMimeType) IsExact() bool { + if e != nil { + switch *e { + case "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff": + return true + } + } + return false +} + +type ImageDelta struct { + Data *string `json:"data,omitzero"` + MimeType *ImageDeltaMimeType `json:"mime_type,omitzero"` + Resolution *MediaResolution `json:"resolution,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"image" json:"type"` + URI *string `json:"uri,omitzero"` +} + +func (i ImageDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *ImageDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *ImageDelta) GetData() *string { + if i == nil { + return nil + } + return i.Data +} + +func (i *ImageDelta) GetMimeType() *ImageDeltaMimeType { + if i == nil { + return nil + } + return i.MimeType +} + +func (i *ImageDelta) GetResolution() *MediaResolution { + if i == nil { + return nil + } + return i.Resolution +} + +func (i *ImageDelta) GetType() string { + return "image" +} + +func (i *ImageDelta) GetURI() *string { + if i == nil { + return nil + } + return i.URI +} diff --git a/internal/sdk/models/interactions/imageresponseformat.go b/internal/sdk/models/interactions/imageresponseformat.go new file mode 100644 index 0000000..c77538d --- /dev/null +++ b/internal/sdk/models/interactions/imageresponseformat.go @@ -0,0 +1,188 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ImageResponseFormatAspectRatio - The aspect ratio for the image output. +type ImageResponseFormatAspectRatio string + +const ( + ImageResponseFormatAspectRatioEleven ImageResponseFormatAspectRatio = "1:1" + ImageResponseFormatAspectRatioTwentyThree ImageResponseFormatAspectRatio = "2:3" + ImageResponseFormatAspectRatioThirtyTwo ImageResponseFormatAspectRatio = "3:2" + ImageResponseFormatAspectRatioThirtyFour ImageResponseFormatAspectRatio = "3:4" + ImageResponseFormatAspectRatioFortyThree ImageResponseFormatAspectRatio = "4:3" + ImageResponseFormatAspectRatioFortyFive ImageResponseFormatAspectRatio = "4:5" + ImageResponseFormatAspectRatioFiftyFour ImageResponseFormatAspectRatio = "5:4" + ImageResponseFormatAspectRatioNineHundredAndSixteen ImageResponseFormatAspectRatio = "9:16" + ImageResponseFormatAspectRatioOneHundredAndSixtyNine ImageResponseFormatAspectRatio = "16:9" + ImageResponseFormatAspectRatioTwoHundredAndNineteen ImageResponseFormatAspectRatio = "21:9" + ImageResponseFormatAspectRatioEighteen ImageResponseFormatAspectRatio = "1:8" + ImageResponseFormatAspectRatioEightyOne ImageResponseFormatAspectRatio = "8:1" + ImageResponseFormatAspectRatioFourteen ImageResponseFormatAspectRatio = "1:4" + ImageResponseFormatAspectRatioFortyOne ImageResponseFormatAspectRatio = "4:1" +) + +func (e ImageResponseFormatAspectRatio) ToPointer() *ImageResponseFormatAspectRatio { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageResponseFormatAspectRatio) IsExact() bool { + if e != nil { + switch *e { + case "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1": + return true + } + } + return false +} + +// ImageResponseFormatDelivery - The delivery mode for the image output. +type ImageResponseFormatDelivery string + +const ( + ImageResponseFormatDeliveryInline ImageResponseFormatDelivery = "inline" + ImageResponseFormatDeliveryURI ImageResponseFormatDelivery = "uri" +) + +func (e ImageResponseFormatDelivery) ToPointer() *ImageResponseFormatDelivery { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageResponseFormatDelivery) IsExact() bool { + if e != nil { + switch *e { + case "inline", "uri": + return true + } + } + return false +} + +// ImageResponseFormatImageSize - The size of the image output. +type ImageResponseFormatImageSize string + +const ( + ImageResponseFormatImageSizeFiveHundredAndTwelve ImageResponseFormatImageSize = "512" + ImageResponseFormatImageSizeOneK ImageResponseFormatImageSize = "1K" + ImageResponseFormatImageSizeTwoK ImageResponseFormatImageSize = "2K" + ImageResponseFormatImageSizeFourK ImageResponseFormatImageSize = "4K" +) + +func (e ImageResponseFormatImageSize) ToPointer() *ImageResponseFormatImageSize { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ImageResponseFormatImageSize) IsExact() bool { + if e != nil { + switch *e { + case "512", "1K", "2K", "4K": + return true + } + } + return false +} + +// ImageResponseFormatMimeType - The MIME type of the image output. +type ImageResponseFormatMimeType string + +const ( + ImageResponseFormatMimeTypeImageJpeg ImageResponseFormatMimeType = "image/jpeg" +) + +func (e ImageResponseFormatMimeType) ToPointer() *ImageResponseFormatMimeType { + return &e +} +func (e *ImageResponseFormatMimeType) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "image/jpeg": + *e = ImageResponseFormatMimeType(v) + return nil + default: + return fmt.Errorf("invalid value for ImageResponseFormatMimeType: %v", v) + } +} + +// ImageResponseFormat - Configuration for image output format. +type ImageResponseFormat struct { + // The aspect ratio for the image output. + AspectRatio *ImageResponseFormatAspectRatio `json:"aspect_ratio,omitzero"` + // The delivery mode for the image output. + Delivery *ImageResponseFormatDelivery `json:"delivery,omitzero"` + // The size of the image output. + ImageSize *ImageResponseFormatImageSize `json:"image_size,omitzero"` + // The MIME type of the image output. + MimeType *ImageResponseFormatMimeType `json:"mime_type,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"image" json:"type"` +} + +func (i ImageResponseFormat) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *ImageResponseFormat) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *ImageResponseFormat) GetAspectRatio() *ImageResponseFormatAspectRatio { + if i == nil { + return nil + } + return i.AspectRatio +} + +func (i *ImageResponseFormat) GetDelivery() *ImageResponseFormatDelivery { + if i == nil { + return nil + } + return i.Delivery +} + +func (i *ImageResponseFormat) GetImageSize() *ImageResponseFormatImageSize { + if i == nil { + return nil + } + return i.ImageSize +} + +func (i *ImageResponseFormat) GetMimeType() *ImageResponseFormatMimeType { + if i == nil { + return nil + } + return i.MimeType +} + +func (i *ImageResponseFormat) GetType() string { + return "image" +} diff --git a/internal/sdk/models/interactions/interaction.go b/internal/sdk/models/interactions/interaction.go new file mode 100644 index 0000000..d1cb5a7 --- /dev/null +++ b/internal/sdk/models/interactions/interaction.go @@ -0,0 +1,756 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type InteractionAgentConfigType string + +const ( + InteractionAgentConfigTypeAntigravity InteractionAgentConfigType = "antigravity" + InteractionAgentConfigTypeCodeMender InteractionAgentConfigType = "code-mender" + InteractionAgentConfigTypeDeepResearch InteractionAgentConfigType = "deep-research" + InteractionAgentConfigTypeDynamic InteractionAgentConfigType = "dynamic" + InteractionAgentConfigTypeUnknown InteractionAgentConfigType = "UNKNOWN" +) + +// InteractionAgentConfig - Configuration parameters for the agent interaction. +type InteractionAgentConfig struct { + AntigravityAgentConfig *AntigravityAgentConfig `queryParam:"inline" union:"member"` + CodeMenderAgentConfig *CodeMenderAgentConfig `queryParam:"inline" union:"member"` + DeepResearchAgentConfig *DeepResearchAgentConfig `queryParam:"inline" union:"member"` + DynamicAgentConfig *DynamicAgentConfig `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type InteractionAgentConfigType +} + +func CreateInteractionAgentConfigAntigravity(antigravity AntigravityAgentConfig) InteractionAgentConfig { + typ := InteractionAgentConfigTypeAntigravity + + return InteractionAgentConfig{ + AntigravityAgentConfig: &antigravity, + Type: typ, + } +} + +func CreateInteractionAgentConfigCodeMender(codeMender CodeMenderAgentConfig) InteractionAgentConfig { + typ := InteractionAgentConfigTypeCodeMender + + return InteractionAgentConfig{ + CodeMenderAgentConfig: &codeMender, + Type: typ, + } +} + +func CreateInteractionAgentConfigDeepResearch(deepResearch DeepResearchAgentConfig) InteractionAgentConfig { + typ := InteractionAgentConfigTypeDeepResearch + + return InteractionAgentConfig{ + DeepResearchAgentConfig: &deepResearch, + Type: typ, + } +} + +func CreateInteractionAgentConfigDynamic(dynamic DynamicAgentConfig) InteractionAgentConfig { + typ := InteractionAgentConfigTypeDynamic + + return InteractionAgentConfig{ + DynamicAgentConfig: &dynamic, + Type: typ, + } +} + +func CreateInteractionAgentConfigUnknown(raw json.RawMessage) InteractionAgentConfig { + return InteractionAgentConfig{ + UnknownRaw: raw, + Type: InteractionAgentConfigTypeUnknown, + } +} + +func (u InteractionAgentConfig) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u InteractionAgentConfig) IsUnknown() bool { + return u.Type == InteractionAgentConfigTypeUnknown +} + +func (u *InteractionAgentConfig) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = InteractionAgentConfig{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionAgentConfigTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionAgentConfigTypeUnknown + return nil + } + + switch dis.Type { + case "antigravity": + antigravityAgentConfig := new(AntigravityAgentConfig) + if err := utils.UnmarshalJSON(data, &antigravityAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == antigravity) type AntigravityAgentConfig within InteractionAgentConfig: %w", string(data), err) + } + + u.AntigravityAgentConfig = antigravityAgentConfig + u.Type = InteractionAgentConfigTypeAntigravity + return nil + case "code-mender": + codeMenderAgentConfig := new(CodeMenderAgentConfig) + if err := utils.UnmarshalJSON(data, &codeMenderAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code-mender) type CodeMenderAgentConfig within InteractionAgentConfig: %w", string(data), err) + } + + u.CodeMenderAgentConfig = codeMenderAgentConfig + u.Type = InteractionAgentConfigTypeCodeMender + return nil + case "deep-research": + deepResearchAgentConfig := new(DeepResearchAgentConfig) + if err := utils.UnmarshalJSON(data, &deepResearchAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == deep-research) type DeepResearchAgentConfig within InteractionAgentConfig: %w", string(data), err) + } + + u.DeepResearchAgentConfig = deepResearchAgentConfig + u.Type = InteractionAgentConfigTypeDeepResearch + return nil + case "dynamic": + dynamicAgentConfig := new(DynamicAgentConfig) + if err := utils.UnmarshalJSON(data, &dynamicAgentConfig, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == dynamic) type DynamicAgentConfig within InteractionAgentConfig: %w", string(data), err) + } + + u.DynamicAgentConfig = dynamicAgentConfig + u.Type = InteractionAgentConfigTypeDynamic + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionAgentConfigTypeUnknown + return nil + } + +} + +func (u InteractionAgentConfig) MarshalJSON() ([]byte, error) { + if u.AntigravityAgentConfig != nil { + return utils.MarshalJSON(u.AntigravityAgentConfig, "", true) + } + + if u.CodeMenderAgentConfig != nil { + return utils.MarshalJSON(u.CodeMenderAgentConfig, "", true) + } + + if u.DeepResearchAgentConfig != nil { + return utils.MarshalJSON(u.DeepResearchAgentConfig, "", true) + } + + if u.DynamicAgentConfig != nil { + return utils.MarshalJSON(u.DynamicAgentConfig, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type InteractionAgentConfig: all fields are null") +} + +type InteractionEnvironmentType string + +const ( + InteractionEnvironmentTypeEnvironment InteractionEnvironmentType = "Environment" + InteractionEnvironmentTypeStr InteractionEnvironmentType = "str" + InteractionEnvironmentTypeUnknown InteractionEnvironmentType = "Unknown" +) + +// InteractionEnvironment - The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. +type InteractionEnvironment struct { + Environment *Environment `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type InteractionEnvironmentType +} + +func CreateInteractionEnvironmentEnvironment(environment Environment) InteractionEnvironment { + typ := InteractionEnvironmentTypeEnvironment + + return InteractionEnvironment{ + Environment: &environment, + Type: typ, + } +} + +func CreateInteractionEnvironmentStr(str string) InteractionEnvironment { + typ := InteractionEnvironmentTypeStr + + return InteractionEnvironment{ + Str: &str, + Type: typ, + } +} + +func CreateInteractionEnvironmentUnknown(raw json.RawMessage) InteractionEnvironment { + return InteractionEnvironment{ + UnknownRaw: raw, + Type: InteractionEnvironmentTypeUnknown, + } +} + +func (u InteractionEnvironment) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u InteractionEnvironment) IsUnknown() bool { + return u.Type == InteractionEnvironmentTypeUnknown +} + +func (u *InteractionEnvironment) UnmarshalJSON(data []byte) error { + *u = InteractionEnvironment{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var environment Environment = Environment{} + if err := utils.UnmarshalJSON(data, &environment, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionEnvironmentTypeEnvironment, + Value: &environment, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionEnvironmentTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionEnvironmentTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionEnvironmentTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(InteractionEnvironmentType) + switch best.Type { + case InteractionEnvironmentTypeEnvironment: + u.Environment = best.Value.(*Environment) + return nil + case InteractionEnvironmentTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionEnvironmentTypeUnknown + return nil +} + +func (u InteractionEnvironment) MarshalJSON() ([]byte, error) { + if u.Environment != nil { + return utils.MarshalJSON(u.Environment, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type InteractionEnvironment: all fields are null") +} + +type InteractionResponseFormatType string + +const ( + InteractionResponseFormatTypeResponseFormat InteractionResponseFormatType = "ResponseFormat" + InteractionResponseFormatTypeArrayOfResponseFormat InteractionResponseFormatType = "arrayOfResponseFormat" + InteractionResponseFormatTypeUnknown InteractionResponseFormatType = "Unknown" +) + +// InteractionResponseFormat - Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. +type InteractionResponseFormat struct { + ResponseFormat *ResponseFormat `queryParam:"inline" union:"member"` + ArrayOfResponseFormat []ResponseFormat `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type InteractionResponseFormatType +} + +func CreateInteractionResponseFormatResponseFormat(responseFormat ResponseFormat) InteractionResponseFormat { + typ := InteractionResponseFormatTypeResponseFormat + + return InteractionResponseFormat{ + ResponseFormat: &responseFormat, + Type: typ, + } +} + +func CreateInteractionResponseFormatArrayOfResponseFormat(arrayOfResponseFormat []ResponseFormat) InteractionResponseFormat { + typ := InteractionResponseFormatTypeArrayOfResponseFormat + + return InteractionResponseFormat{ + ArrayOfResponseFormat: arrayOfResponseFormat, + Type: typ, + } +} + +func CreateInteractionResponseFormatUnknown(raw json.RawMessage) InteractionResponseFormat { + return InteractionResponseFormat{ + UnknownRaw: raw, + Type: InteractionResponseFormatTypeUnknown, + } +} + +func (u InteractionResponseFormat) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u InteractionResponseFormat) IsUnknown() bool { + return u.Type == InteractionResponseFormatTypeUnknown +} + +func (u *InteractionResponseFormat) UnmarshalJSON(data []byte) error { + *u = InteractionResponseFormat{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var responseFormat ResponseFormat = ResponseFormat{} + if err := utils.UnmarshalJSON(data, &responseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionResponseFormatTypeResponseFormat, + Value: &responseFormat, + }) + } + + var arrayOfResponseFormat []ResponseFormat = []ResponseFormat{} + if err := utils.UnmarshalJSON(data, &arrayOfResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionResponseFormatTypeArrayOfResponseFormat, + Value: arrayOfResponseFormat, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionResponseFormatTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionResponseFormatTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(InteractionResponseFormatType) + switch best.Type { + case InteractionResponseFormatTypeResponseFormat: + u.ResponseFormat = best.Value.(*ResponseFormat) + return nil + case InteractionResponseFormatTypeArrayOfResponseFormat: + u.ArrayOfResponseFormat = best.Value.([]ResponseFormat) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionResponseFormatTypeUnknown + return nil +} + +func (u InteractionResponseFormat) MarshalJSON() ([]byte, error) { + if u.ResponseFormat != nil { + return utils.MarshalJSON(u.ResponseFormat, "", true) + } + + if u.ArrayOfResponseFormat != nil { + return utils.MarshalJSON(u.ArrayOfResponseFormat, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type InteractionResponseFormat: all fields are null") +} + +// InteractionStatus - Required. Output only. The status of the interaction. +type InteractionStatus string + +const ( + InteractionStatusInProgress InteractionStatus = "in_progress" + InteractionStatusRequiresAction InteractionStatus = "requires_action" + InteractionStatusCompleted InteractionStatus = "completed" + InteractionStatusFailed InteractionStatus = "failed" + InteractionStatusCancelled InteractionStatus = "cancelled" + InteractionStatusIncomplete InteractionStatus = "incomplete" + InteractionStatusBudgetExceeded InteractionStatus = "budget_exceeded" + InteractionStatusQueued InteractionStatus = "queued" +) + +func (e InteractionStatus) ToPointer() *InteractionStatus { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *InteractionStatus) IsExact() bool { + if e != nil { + switch *e { + case "in_progress", "requires_action", "completed", "failed", "cancelled", "incomplete", "budget_exceeded", "queued": + return true + } + } + return false +} + +// The Interaction resource. +type Interaction struct { + // The agent to interact with. + Agent *AgentOption `json:"agent,omitzero"` + // Configuration parameters for the agent interaction. + AgentConfig *InteractionAgentConfig `json:"agent_config,omitzero"` + // Output only. The time at which the response was created in ISO 8601 format + // (YYYY-MM-DDThh:mm:ssZ). + Created *string `json:"created,omitzero"` + // The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + Environment *InteractionEnvironment `json:"environment,omitzero"` + // Output only. The environment ID for the interaction. Only populated if environment + // config is set in the request. + EnvironmentID *string `json:"environment_id,omitzero"` + // Output only. Diagnostic faults / platform errors recorded on the interaction. + Errors []Error `json:"errors,omitzero"` + // Configuration parameters for model interactions. + GenerationConfig *GenerationConfig `json:"generation_config,omitzero"` + // Required. Output only. A unique identifier for the interaction completion. + ID *string `default:"" json:"id"` + // The input for the interaction. + Input *InteractionsInput `json:"input,omitzero"` + // The labels with user-defined metadata for the request. + Labels map[string]string `json:"labels,omitzero"` + // The model that will complete your prompt.\n\nSee [models](https://ai.google.dev/gemini-api/docs/models) for additional details. + Model *Model `default:"gemini-3.6-flash" json:"model"` + // An audio content block. + OutputAudio *AudioContent `json:"output_audio,omitzero"` + // An image content block. + OutputImage *ImageContent `json:"output_image,omitzero"` + // Concatenated text from the last model output in response to the current request. + // + // Note: this is added by the SDK. + OutputText *string `json:"output_text,omitzero"` + // A video content block. + OutputVideo *VideoContent `json:"output_video,omitzero"` + // The ID of the previous interaction, if any. + PreviousInteractionID *string `json:"previous_interaction_id,omitzero"` + // Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + ResponseFormat *InteractionResponseFormat `json:"response_format,omitzero"` + // The mime type of the response. This is required if response_format is set. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ResponseMimeType *string `json:"response_mime_type,omitzero"` + // The requested modalities of the response (TEXT, IMAGE, AUDIO). + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + ResponseModalities []ResponseModality `json:"response_modalities,omitzero"` + // Safety settings for the interaction. + SafetySettings []SafetySetting `json:"safety_settings,omitzero"` + ServiceTier *ServiceTier `json:"service_tier,omitzero"` + // Required. Output only. The status of the interaction. + Status InteractionStatus `json:"status"` + // Output only. The steps that make up the interaction, when included in the response. + Steps []Step `json:"steps,omitzero"` + // System instruction for the interaction. + SystemInstruction *string `json:"system_instruction,omitzero"` + // A list of tool declarations the model may call during interaction. + Tools []Tool `json:"tools,omitzero"` + // Output only. The time at which the response was last updated in ISO 8601 format + // (YYYY-MM-DDThh:mm:ssZ). + Updated *string `json:"updated,omitzero"` + // Statistics on the interaction request's token usage. + Usage *Usage `json:"usage,omitzero"` + // Message for configuring webhook events for a request. + WebhookConfig *WebhookConfig `json:"webhook_config,omitzero"` +} + +func (i Interaction) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *Interaction) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *Interaction) GetAgent() *AgentOption { + if i == nil { + return nil + } + return i.Agent +} + +func (i *Interaction) GetAgentConfig() *InteractionAgentConfig { + if i == nil { + return nil + } + return i.AgentConfig +} + +func (i *Interaction) GetAgentConfigAntigravity() *AntigravityAgentConfig { + if v := i.GetAgentConfig(); v != nil { + return v.AntigravityAgentConfig + } + return nil +} + +func (i *Interaction) GetAgentConfigCodeMender() *CodeMenderAgentConfig { + if v := i.GetAgentConfig(); v != nil { + return v.CodeMenderAgentConfig + } + return nil +} + +func (i *Interaction) GetAgentConfigDeepResearch() *DeepResearchAgentConfig { + if v := i.GetAgentConfig(); v != nil { + return v.DeepResearchAgentConfig + } + return nil +} + +func (i *Interaction) GetAgentConfigDynamic() *DynamicAgentConfig { + if v := i.GetAgentConfig(); v != nil { + return v.DynamicAgentConfig + } + return nil +} + +func (i *Interaction) GetCreated() *string { + if i == nil { + return nil + } + return i.Created +} + +func (i *Interaction) GetEnvironment() *InteractionEnvironment { + if i == nil { + return nil + } + return i.Environment +} + +func (i *Interaction) GetEnvironmentID() *string { + if i == nil { + return nil + } + return i.EnvironmentID +} + +func (i *Interaction) GetErrors() []Error { + if i == nil { + return nil + } + return i.Errors +} + +func (i *Interaction) GetGenerationConfig() *GenerationConfig { + if i == nil { + return nil + } + return i.GenerationConfig +} + +func (i *Interaction) GetID() *string { + if i == nil { + return nil + } + return i.ID +} + +func (i *Interaction) GetInput() *InteractionsInput { + if i == nil { + return nil + } + return i.Input +} + +func (i *Interaction) GetLabels() map[string]string { + if i == nil { + return nil + } + return i.Labels +} + +func (i *Interaction) GetModel() *Model { + if i == nil { + return nil + } + return i.Model +} + +func (i *Interaction) GetOutputAudio() *AudioContent { + if i == nil { + return nil + } + return i.OutputAudio +} + +func (i *Interaction) GetOutputImage() *ImageContent { + if i == nil { + return nil + } + return i.OutputImage +} + +func (i *Interaction) GetOutputText() *string { + if i == nil { + return nil + } + return i.OutputText +} + +func (i *Interaction) GetOutputVideo() *VideoContent { + if i == nil { + return nil + } + return i.OutputVideo +} + +func (i *Interaction) GetPreviousInteractionID() *string { + if i == nil { + return nil + } + return i.PreviousInteractionID +} + +func (i *Interaction) GetResponseFormat() *InteractionResponseFormat { + if i == nil { + return nil + } + return i.ResponseFormat +} + +func (i *Interaction) GetResponseMimeType() *string { + if i == nil { + return nil + } + return i.ResponseMimeType +} + +func (i *Interaction) GetResponseModalities() []ResponseModality { + if i == nil { + return nil + } + return i.ResponseModalities +} + +func (i *Interaction) GetSafetySettings() []SafetySetting { + if i == nil { + return nil + } + return i.SafetySettings +} + +func (i *Interaction) GetServiceTier() *ServiceTier { + if i == nil { + return nil + } + return i.ServiceTier +} + +func (i *Interaction) GetStatus() InteractionStatus { + if i == nil { + return InteractionStatus("") + } + return i.Status +} + +func (i *Interaction) GetSteps() []Step { + if i == nil { + return nil + } + return i.Steps +} + +func (i *Interaction) GetSystemInstruction() *string { + if i == nil { + return nil + } + return i.SystemInstruction +} + +func (i *Interaction) GetTools() []Tool { + if i == nil { + return nil + } + return i.Tools +} + +func (i *Interaction) GetUpdated() *string { + if i == nil { + return nil + } + return i.Updated +} + +func (i *Interaction) GetUsage() *Usage { + if i == nil { + return nil + } + return i.Usage +} + +func (i *Interaction) GetWebhookConfig() *WebhookConfig { + if i == nil { + return nil + } + return i.WebhookConfig +} diff --git a/internal/sdk/models/interactions/interactioncompletedevent.go b/internal/sdk/models/interactions/interactioncompletedevent.go new file mode 100644 index 0000000..4765b27 --- /dev/null +++ b/internal/sdk/models/interactions/interactioncompletedevent.go @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type InteractionCompletedEvent struct { + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"interaction.completed" json:"event_type"` + // Partial interaction resource emitted by interaction lifecycle SSE events. + // Streaming lifecycle payloads may omit fields that are only available on + // full non-streaming Interaction responses. + // + Interaction InteractionSseEventInteraction `json:"interaction"` +} + +func (i InteractionCompletedEvent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *InteractionCompletedEvent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *InteractionCompletedEvent) GetEventID() *string { + if i == nil { + return nil + } + return i.EventID +} + +func (i *InteractionCompletedEvent) GetEventType() string { + return "interaction.completed" +} + +func (i *InteractionCompletedEvent) GetInteraction() InteractionSseEventInteraction { + if i == nil { + return InteractionSseEventInteraction{} + } + return i.Interaction +} diff --git a/internal/sdk/models/interactions/interactioncreatedevent.go b/internal/sdk/models/interactions/interactioncreatedevent.go new file mode 100644 index 0000000..189008a --- /dev/null +++ b/internal/sdk/models/interactions/interactioncreatedevent.go @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type InteractionCreatedEvent struct { + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"interaction.created" json:"event_type"` + // Partial interaction resource emitted by interaction lifecycle SSE events. + // Streaming lifecycle payloads may omit fields that are only available on + // full non-streaming Interaction responses. + // + Interaction InteractionSseEventInteraction `json:"interaction"` +} + +func (i InteractionCreatedEvent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *InteractionCreatedEvent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *InteractionCreatedEvent) GetEventID() *string { + if i == nil { + return nil + } + return i.EventID +} + +func (i *InteractionCreatedEvent) GetEventType() string { + return "interaction.created" +} + +func (i *InteractionCreatedEvent) GetInteraction() InteractionSseEventInteraction { + if i == nil { + return InteractionSseEventInteraction{} + } + return i.Interaction +} diff --git a/internal/sdk/models/interactions/interactionsinput.go b/internal/sdk/models/interactions/interactionsinput.go new file mode 100644 index 0000000..3c30d85 --- /dev/null +++ b/internal/sdk/models/interactions/interactionsinput.go @@ -0,0 +1,193 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type InteractionsInputType string + +const ( + InteractionsInputTypeContent InteractionsInputType = "Content" + InteractionsInputTypeArrayOfStep InteractionsInputType = "arrayOfStep" + InteractionsInputTypeArrayOfContent InteractionsInputType = "arrayOfContent" + InteractionsInputTypeStr InteractionsInputType = "str" + InteractionsInputTypeUnknown InteractionsInputType = "Unknown" +) + +// InteractionsInput - The input for the interaction. +type InteractionsInput struct { + Content *Content `queryParam:"inline" union:"member"` + ArrayOfStep []Step `queryParam:"inline" union:"member"` + ArrayOfContent []Content `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type InteractionsInputType +} + +func CreateInteractionsInputContent(content Content) InteractionsInput { + typ := InteractionsInputTypeContent + + return InteractionsInput{ + Content: &content, + Type: typ, + } +} + +func CreateInteractionsInputArrayOfStep(arrayOfStep []Step) InteractionsInput { + typ := InteractionsInputTypeArrayOfStep + + return InteractionsInput{ + ArrayOfStep: arrayOfStep, + Type: typ, + } +} + +func CreateInteractionsInputArrayOfContent(arrayOfContent []Content) InteractionsInput { + typ := InteractionsInputTypeArrayOfContent + + return InteractionsInput{ + ArrayOfContent: arrayOfContent, + Type: typ, + } +} + +func CreateInteractionsInputStr(str string) InteractionsInput { + typ := InteractionsInputTypeStr + + return InteractionsInput{ + Str: &str, + Type: typ, + } +} + +func CreateInteractionsInputUnknown(raw json.RawMessage) InteractionsInput { + return InteractionsInput{ + UnknownRaw: raw, + Type: InteractionsInputTypeUnknown, + } +} + +func (u InteractionsInput) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u InteractionsInput) IsUnknown() bool { + return u.Type == InteractionsInputTypeUnknown +} + +func (u *InteractionsInput) UnmarshalJSON(data []byte) error { + *u = InteractionsInput{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var content Content = Content{} + if err := utils.UnmarshalJSON(data, &content, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionsInputTypeContent, + Value: &content, + }) + } + + var arrayOfStep []Step = []Step{} + if err := utils.UnmarshalJSON(data, &arrayOfStep, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionsInputTypeArrayOfStep, + Value: arrayOfStep, + }) + } + + var arrayOfContent []Content = []Content{} + if err := utils.UnmarshalJSON(data, &arrayOfContent, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionsInputTypeArrayOfContent, + Value: arrayOfContent, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: InteractionsInputTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionsInputTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionsInputTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(InteractionsInputType) + switch best.Type { + case InteractionsInputTypeContent: + u.Content = best.Value.(*Content) + return nil + case InteractionsInputTypeArrayOfStep: + u.ArrayOfStep = best.Value.([]Step) + return nil + case InteractionsInputTypeArrayOfContent: + u.ArrayOfContent = best.Value.([]Content) + return nil + case InteractionsInputTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionsInputTypeUnknown + return nil +} + +func (u InteractionsInput) MarshalJSON() ([]byte, error) { + if u.Content != nil { + return utils.MarshalJSON(u.Content, "", true) + } + + if u.ArrayOfStep != nil { + return utils.MarshalJSON(u.ArrayOfStep, "", true) + } + + if u.ArrayOfContent != nil { + return utils.MarshalJSON(u.ArrayOfContent, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type InteractionsInput: all fields are null") +} diff --git a/internal/sdk/models/interactions/interactionsseevent.go b/internal/sdk/models/interactions/interactionsseevent.go new file mode 100644 index 0000000..b45d595 --- /dev/null +++ b/internal/sdk/models/interactions/interactionsseevent.go @@ -0,0 +1,261 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type InteractionSSEEventType string + +const ( + InteractionSSEEventTypeError InteractionSSEEventType = "error" + InteractionSSEEventTypeInteractionCompleted InteractionSSEEventType = "interaction.completed" + InteractionSSEEventTypeInteractionCreated InteractionSSEEventType = "interaction.created" + InteractionSSEEventTypeInteractionStatusUpdate InteractionSSEEventType = "interaction.status_update" + InteractionSSEEventTypeStepDelta InteractionSSEEventType = "step.delta" + InteractionSSEEventTypeStepStart InteractionSSEEventType = "step.start" + InteractionSSEEventTypeStepStop InteractionSSEEventType = "step.stop" + InteractionSSEEventTypeUnknown InteractionSSEEventType = "UNKNOWN" +) + +type InteractionSSEEvent struct { + ErrorEvent *ErrorEvent `queryParam:"inline" union:"member"` + InteractionCompletedEvent *InteractionCompletedEvent `queryParam:"inline" union:"member"` + InteractionCreatedEvent *InteractionCreatedEvent `queryParam:"inline" union:"member"` + InteractionStatusUpdate *InteractionStatusUpdate `queryParam:"inline" union:"member"` + StepDelta *StepDelta `queryParam:"inline" union:"member"` + StepStart *StepStart `queryParam:"inline" union:"member"` + StepStop *StepStop `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type InteractionSSEEventType +} + +func CreateInteractionSSEEventError(errorT ErrorEvent) InteractionSSEEvent { + typ := InteractionSSEEventTypeError + + return InteractionSSEEvent{ + ErrorEvent: &errorT, + Type: typ, + } +} + +func CreateInteractionSSEEventInteractionCompleted(interactionCompleted InteractionCompletedEvent) InteractionSSEEvent { + typ := InteractionSSEEventTypeInteractionCompleted + + return InteractionSSEEvent{ + InteractionCompletedEvent: &interactionCompleted, + Type: typ, + } +} + +func CreateInteractionSSEEventInteractionCreated(interactionCreated InteractionCreatedEvent) InteractionSSEEvent { + typ := InteractionSSEEventTypeInteractionCreated + + return InteractionSSEEvent{ + InteractionCreatedEvent: &interactionCreated, + Type: typ, + } +} + +func CreateInteractionSSEEventInteractionStatusUpdate(interactionStatusUpdate InteractionStatusUpdate) InteractionSSEEvent { + typ := InteractionSSEEventTypeInteractionStatusUpdate + + return InteractionSSEEvent{ + InteractionStatusUpdate: &interactionStatusUpdate, + Type: typ, + } +} + +func CreateInteractionSSEEventStepDelta(stepDelta StepDelta) InteractionSSEEvent { + typ := InteractionSSEEventTypeStepDelta + + return InteractionSSEEvent{ + StepDelta: &stepDelta, + Type: typ, + } +} + +func CreateInteractionSSEEventStepStart(stepStart StepStart) InteractionSSEEvent { + typ := InteractionSSEEventTypeStepStart + + return InteractionSSEEvent{ + StepStart: &stepStart, + Type: typ, + } +} + +func CreateInteractionSSEEventStepStop(stepStop StepStop) InteractionSSEEvent { + typ := InteractionSSEEventTypeStepStop + + return InteractionSSEEvent{ + StepStop: &stepStop, + Type: typ, + } +} + +func CreateInteractionSSEEventUnknown(raw json.RawMessage) InteractionSSEEvent { + return InteractionSSEEvent{ + UnknownRaw: raw, + Type: InteractionSSEEventTypeUnknown, + } +} + +func (u InteractionSSEEvent) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u InteractionSSEEvent) IsUnknown() bool { + return u.Type == InteractionSSEEventTypeUnknown +} + +func (u *InteractionSSEEvent) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = InteractionSSEEvent{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + EventType string `json:"event_type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionSSEEventTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionSSEEventTypeUnknown + return nil + } + + switch dis.EventType { + case "error": + errorEvent := new(ErrorEvent) + if err := utils.UnmarshalJSON(data, &errorEvent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == error) type ErrorEvent within InteractionSSEEvent: %w", string(data), err) + } + + u.ErrorEvent = errorEvent + u.Type = InteractionSSEEventTypeError + return nil + case "interaction.completed": + interactionCompletedEvent := new(InteractionCompletedEvent) + if err := utils.UnmarshalJSON(data, &interactionCompletedEvent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == interaction.completed) type InteractionCompletedEvent within InteractionSSEEvent: %w", string(data), err) + } + + u.InteractionCompletedEvent = interactionCompletedEvent + u.Type = InteractionSSEEventTypeInteractionCompleted + return nil + case "interaction.created": + interactionCreatedEvent := new(InteractionCreatedEvent) + if err := utils.UnmarshalJSON(data, &interactionCreatedEvent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == interaction.created) type InteractionCreatedEvent within InteractionSSEEvent: %w", string(data), err) + } + + u.InteractionCreatedEvent = interactionCreatedEvent + u.Type = InteractionSSEEventTypeInteractionCreated + return nil + case "interaction.status_update": + interactionStatusUpdate := new(InteractionStatusUpdate) + if err := utils.UnmarshalJSON(data, &interactionStatusUpdate, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == interaction.status_update) type InteractionStatusUpdate within InteractionSSEEvent: %w", string(data), err) + } + + u.InteractionStatusUpdate = interactionStatusUpdate + u.Type = InteractionSSEEventTypeInteractionStatusUpdate + return nil + case "step.delta": + stepDelta := new(StepDelta) + if err := utils.UnmarshalJSON(data, &stepDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == step.delta) type StepDelta within InteractionSSEEvent: %w", string(data), err) + } + + u.StepDelta = stepDelta + u.Type = InteractionSSEEventTypeStepDelta + return nil + case "step.start": + stepStart := new(StepStart) + if err := utils.UnmarshalJSON(data, &stepStart, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == step.start) type StepStart within InteractionSSEEvent: %w", string(data), err) + } + + u.StepStart = stepStart + u.Type = InteractionSSEEventTypeStepStart + return nil + case "step.stop": + stepStop := new(StepStop) + if err := utils.UnmarshalJSON(data, &stepStop, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (EventType == step.stop) type StepStop within InteractionSSEEvent: %w", string(data), err) + } + + u.StepStop = stepStop + u.Type = InteractionSSEEventTypeStepStop + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = InteractionSSEEventTypeUnknown + return nil + } + +} + +func (u InteractionSSEEvent) MarshalJSON() ([]byte, error) { + if u.ErrorEvent != nil { + return utils.MarshalJSON(u.ErrorEvent, "", true) + } + + if u.InteractionCompletedEvent != nil { + return utils.MarshalJSON(u.InteractionCompletedEvent, "", true) + } + + if u.InteractionCreatedEvent != nil { + return utils.MarshalJSON(u.InteractionCreatedEvent, "", true) + } + + if u.InteractionStatusUpdate != nil { + return utils.MarshalJSON(u.InteractionStatusUpdate, "", true) + } + + if u.StepDelta != nil { + return utils.MarshalJSON(u.StepDelta, "", true) + } + + if u.StepStart != nil { + return utils.MarshalJSON(u.StepStart, "", true) + } + + if u.StepStop != nil { + return utils.MarshalJSON(u.StepStop, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type InteractionSSEEvent: all fields are null") +} diff --git a/internal/sdk/models/interactions/interactionsseeventinteraction.go b/internal/sdk/models/interactions/interactionsseeventinteraction.go new file mode 100644 index 0000000..1a282d0 --- /dev/null +++ b/internal/sdk/models/interactions/interactionsseeventinteraction.go @@ -0,0 +1,154 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// InteractionSseEventInteractionStatus - Required. Output only. The status of the interaction. +type InteractionSseEventInteractionStatus string + +const ( + InteractionSseEventInteractionStatusInProgress InteractionSseEventInteractionStatus = "in_progress" + InteractionSseEventInteractionStatusRequiresAction InteractionSseEventInteractionStatus = "requires_action" + InteractionSseEventInteractionStatusCompleted InteractionSseEventInteractionStatus = "completed" + InteractionSseEventInteractionStatusFailed InteractionSseEventInteractionStatus = "failed" + InteractionSseEventInteractionStatusCancelled InteractionSseEventInteractionStatus = "cancelled" + InteractionSseEventInteractionStatusIncomplete InteractionSseEventInteractionStatus = "incomplete" +) + +func (e InteractionSseEventInteractionStatus) ToPointer() *InteractionSseEventInteractionStatus { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *InteractionSseEventInteractionStatus) IsExact() bool { + if e != nil { + switch *e { + case "in_progress", "requires_action", "completed", "failed", "cancelled", "incomplete": + return true + } + } + return false +} + +// InteractionSseEventInteraction - Partial interaction resource emitted by interaction lifecycle SSE events. +// Streaming lifecycle payloads may omit fields that are only available on +// full non-streaming Interaction responses. +type InteractionSseEventInteraction struct { + // The agent to interact with. + Agent *string `json:"agent,omitzero"` + // Output only. The time at which the response was created in ISO 8601 format. + Created *string `json:"created,omitzero"` + // Required. Output only. A unique identifier for the interaction completion. + ID string `json:"id"` + // The model that will complete your prompt. + Model *string `json:"model,omitzero"` + // Output only. The resource type. + Object *string `json:"object,omitzero"` + ServiceTier *ServiceTier `json:"service_tier,omitzero"` + // Required. Output only. The status of the interaction. + Status InteractionSseEventInteractionStatus `json:"status"` + // Output only. The steps that make up the interaction, if included in this event. + Steps []Step `json:"steps,omitzero"` + // Output only. The time at which the response was last updated in ISO 8601 format. + Updated *string `json:"updated,omitzero"` + // Statistics on the interaction request's token usage. + Usage *Usage `json:"usage,omitzero"` +} + +func (i InteractionSseEventInteraction) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *InteractionSseEventInteraction) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *InteractionSseEventInteraction) GetAgent() *string { + if i == nil { + return nil + } + return i.Agent +} + +func (i *InteractionSseEventInteraction) GetCreated() *string { + if i == nil { + return nil + } + return i.Created +} + +func (i *InteractionSseEventInteraction) GetID() string { + if i == nil { + return "" + } + return i.ID +} + +func (i *InteractionSseEventInteraction) GetModel() *string { + if i == nil { + return nil + } + return i.Model +} + +func (i *InteractionSseEventInteraction) GetObject() *string { + if i == nil { + return nil + } + return i.Object +} + +func (i *InteractionSseEventInteraction) GetServiceTier() *ServiceTier { + if i == nil { + return nil + } + return i.ServiceTier +} + +func (i *InteractionSseEventInteraction) GetStatus() InteractionSseEventInteractionStatus { + if i == nil { + return InteractionSseEventInteractionStatus("") + } + return i.Status +} + +func (i *InteractionSseEventInteraction) GetSteps() []Step { + if i == nil { + return nil + } + return i.Steps +} + +func (i *InteractionSseEventInteraction) GetUpdated() *string { + if i == nil { + return nil + } + return i.Updated +} + +func (i *InteractionSseEventInteraction) GetUsage() *Usage { + if i == nil { + return nil + } + return i.Usage +} diff --git a/internal/sdk/models/interactions/interactionssestreamevent.go b/internal/sdk/models/interactions/interactionssestreamevent.go new file mode 100644 index 0000000..5cc4c61 --- /dev/null +++ b/internal/sdk/models/interactions/interactionssestreamevent.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type InteractionSSEStreamEvent struct { + Data InteractionSSEEvent `json:"data"` +} + +func (i *InteractionSSEStreamEvent) GetData() InteractionSSEEvent { + if i == nil { + return InteractionSSEEvent{} + } + return i.Data +} + +func (i *InteractionSSEStreamEvent) GetDataError() *ErrorEvent { + return i.GetData().ErrorEvent +} + +func (i *InteractionSSEStreamEvent) GetDataInteractionCompleted() *InteractionCompletedEvent { + return i.GetData().InteractionCompletedEvent +} + +func (i *InteractionSSEStreamEvent) GetDataInteractionCreated() *InteractionCreatedEvent { + return i.GetData().InteractionCreatedEvent +} + +func (i *InteractionSSEStreamEvent) GetDataInteractionStatusUpdate() *InteractionStatusUpdate { + return i.GetData().InteractionStatusUpdate +} + +func (i *InteractionSSEStreamEvent) GetDataStepDelta() *StepDelta { + return i.GetData().StepDelta +} + +func (i *InteractionSSEStreamEvent) GetDataStepStart() *StepStart { + return i.GetData().StepStart +} + +func (i *InteractionSSEStreamEvent) GetDataStepStop() *StepStop { + return i.GetData().StepStop +} + +func (i InteractionSSEStreamEvent) GetEventEncoding(event string) (string, error) { + return "application/json", nil +} diff --git a/internal/sdk/models/interactions/interactionstatusupdate.go b/internal/sdk/models/interactions/interactionstatusupdate.go new file mode 100644 index 0000000..9d70b2b --- /dev/null +++ b/internal/sdk/models/interactions/interactionstatusupdate.go @@ -0,0 +1,95 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type InteractionStatusUpdateStatus string + +const ( + InteractionStatusUpdateStatusInProgress InteractionStatusUpdateStatus = "in_progress" + InteractionStatusUpdateStatusRequiresAction InteractionStatusUpdateStatus = "requires_action" + InteractionStatusUpdateStatusCompleted InteractionStatusUpdateStatus = "completed" + InteractionStatusUpdateStatusFailed InteractionStatusUpdateStatus = "failed" + InteractionStatusUpdateStatusCancelled InteractionStatusUpdateStatus = "cancelled" + InteractionStatusUpdateStatusIncomplete InteractionStatusUpdateStatus = "incomplete" + InteractionStatusUpdateStatusBudgetExceeded InteractionStatusUpdateStatus = "budget_exceeded" + InteractionStatusUpdateStatusQueued InteractionStatusUpdateStatus = "queued" +) + +func (e InteractionStatusUpdateStatus) ToPointer() *InteractionStatusUpdateStatus { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *InteractionStatusUpdateStatus) IsExact() bool { + if e != nil { + switch *e { + case "in_progress", "requires_action", "completed", "failed", "cancelled", "incomplete", "budget_exceeded", "queued": + return true + } + } + return false +} + +type InteractionStatusUpdate struct { + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"interaction.status_update" json:"event_type"` + InteractionID string `json:"interaction_id"` + Status InteractionStatusUpdateStatus `json:"status"` +} + +func (i InteractionStatusUpdate) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(i, "", false) +} + +func (i *InteractionStatusUpdate) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &i, "", false, nil); err != nil { + return err + } + return nil +} + +func (i *InteractionStatusUpdate) GetEventID() *string { + if i == nil { + return nil + } + return i.EventID +} + +func (i *InteractionStatusUpdate) GetEventType() string { + return "interaction.status_update" +} + +func (i *InteractionStatusUpdate) GetInteractionID() string { + if i == nil { + return "" + } + return i.InteractionID +} + +func (i *InteractionStatusUpdate) GetStatus() InteractionStatusUpdateStatus { + if i == nil { + return InteractionStatusUpdateStatus("") + } + return i.Status +} diff --git a/internal/sdk/models/interactions/mcpserver.go b/internal/sdk/models/interactions/mcpserver.go new file mode 100644 index 0000000..24c9ea0 --- /dev/null +++ b/internal/sdk/models/interactions/mcpserver.go @@ -0,0 +1,79 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// A MCPServer is a server that can be called by the model to perform actions. +type MCPServer struct { + // The allowed tools. + AllowedTools []AllowedTools `json:"allowed_tools,omitzero"` + // Optional: Fields for authentication headers, timeouts, etc., if needed. + Headers map[string]string `json:"headers,omitzero"` + // The name of the MCPServer. + Name *string `json:"name,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"mcp_server" json:"type"` + // The full URL for the MCPServer endpoint. + // Example: "https://api.example.com/mcp" + URL *string `json:"url,omitzero"` +} + +func (m MCPServer) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServer) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *MCPServer) GetAllowedTools() []AllowedTools { + if m == nil { + return nil + } + return m.AllowedTools +} + +func (m *MCPServer) GetHeaders() map[string]string { + if m == nil { + return nil + } + return m.Headers +} + +func (m *MCPServer) GetName() *string { + if m == nil { + return nil + } + return m.Name +} + +func (m *MCPServer) GetType() string { + return "mcp_server" +} + +func (m *MCPServer) GetURL() *string { + if m == nil { + return nil + } + return m.URL +} diff --git a/internal/sdk/models/interactions/mcpservertoolcalldelta.go b/internal/sdk/models/interactions/mcpservertoolcalldelta.go new file mode 100644 index 0000000..e3d4e4f --- /dev/null +++ b/internal/sdk/models/interactions/mcpservertoolcalldelta.go @@ -0,0 +1,65 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type MCPServerToolCallDelta struct { + Arguments map[string]any `json:"arguments"` + Name string `json:"name"` + ServerName string `json:"server_name"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"mcp_server_tool_call" json:"type"` +} + +func (m MCPServerToolCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServerToolCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *MCPServerToolCallDelta) GetArguments() map[string]any { + if m == nil { + return map[string]any{} + } + return m.Arguments +} + +func (m *MCPServerToolCallDelta) GetName() string { + if m == nil { + return "" + } + return m.Name +} + +func (m *MCPServerToolCallDelta) GetServerName() string { + if m == nil { + return "" + } + return m.ServerName +} + +func (m *MCPServerToolCallDelta) GetType() string { + return "mcp_server_tool_call" +} diff --git a/internal/sdk/models/interactions/mcpservertoolcallstep.go b/internal/sdk/models/interactions/mcpservertoolcallstep.go new file mode 100644 index 0000000..66d46f4 --- /dev/null +++ b/internal/sdk/models/interactions/mcpservertoolcallstep.go @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// MCPServerToolCallStep - MCPServer tool call step. +type MCPServerToolCallStep struct { + // Required. The JSON object of arguments for the function. + Arguments map[string]any `json:"arguments"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // Required. The name of the tool which was called. + Name string `json:"name"` + // Required. The name of the used MCP server. + ServerName string `json:"server_name"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"mcp_server_tool_call" json:"type"` +} + +func (m MCPServerToolCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServerToolCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *MCPServerToolCallStep) GetArguments() map[string]any { + if m == nil { + return map[string]any{} + } + return m.Arguments +} + +func (m *MCPServerToolCallStep) GetID() string { + if m == nil { + return "" + } + return m.ID +} + +func (m *MCPServerToolCallStep) GetName() string { + if m == nil { + return "" + } + return m.Name +} + +func (m *MCPServerToolCallStep) GetServerName() string { + if m == nil { + return "" + } + return m.ServerName +} + +func (m *MCPServerToolCallStep) GetType() string { + return "mcp_server_tool_call" +} diff --git a/internal/sdk/models/interactions/mcpservertoolresultdelta.go b/internal/sdk/models/interactions/mcpservertoolresultdelta.go new file mode 100644 index 0000000..1fb6e5f --- /dev/null +++ b/internal/sdk/models/interactions/mcpservertoolresultdelta.go @@ -0,0 +1,224 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type MCPServerToolResultDeltaResult struct { +} + +func (m MCPServerToolResultDeltaResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServerToolResultDeltaResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +type MCPServerToolResultDeltaResultUnionType string + +const ( + MCPServerToolResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent MCPServerToolResultDeltaResultUnionType = "arrayOfFunctionResultSubcontent" + MCPServerToolResultDeltaResultUnionTypeMCPServerToolResultDeltaResult MCPServerToolResultDeltaResultUnionType = "MCPServerToolResultDelta_result" + MCPServerToolResultDeltaResultUnionTypeStr MCPServerToolResultDeltaResultUnionType = "str" + MCPServerToolResultDeltaResultUnionTypeUnknown MCPServerToolResultDeltaResultUnionType = "Unknown" +) + +type MCPServerToolResultDeltaResultUnion struct { + ArrayOfFunctionResultSubcontent []FunctionResultSubcontent `queryParam:"inline" union:"member"` + MCPServerToolResultDeltaResult *MCPServerToolResultDeltaResult `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type MCPServerToolResultDeltaResultUnionType +} + +func CreateMCPServerToolResultDeltaResultUnionArrayOfFunctionResultSubcontent(arrayOfFunctionResultSubcontent []FunctionResultSubcontent) MCPServerToolResultDeltaResultUnion { + typ := MCPServerToolResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent + + return MCPServerToolResultDeltaResultUnion{ + ArrayOfFunctionResultSubcontent: arrayOfFunctionResultSubcontent, + Type: typ, + } +} + +func CreateMCPServerToolResultDeltaResultUnionMCPServerToolResultDeltaResult(mcpServerToolResultDeltaResult MCPServerToolResultDeltaResult) MCPServerToolResultDeltaResultUnion { + typ := MCPServerToolResultDeltaResultUnionTypeMCPServerToolResultDeltaResult + + return MCPServerToolResultDeltaResultUnion{ + MCPServerToolResultDeltaResult: &mcpServerToolResultDeltaResult, + Type: typ, + } +} + +func CreateMCPServerToolResultDeltaResultUnionStr(str string) MCPServerToolResultDeltaResultUnion { + typ := MCPServerToolResultDeltaResultUnionTypeStr + + return MCPServerToolResultDeltaResultUnion{ + Str: &str, + Type: typ, + } +} + +func CreateMCPServerToolResultDeltaResultUnionUnknown(raw json.RawMessage) MCPServerToolResultDeltaResultUnion { + return MCPServerToolResultDeltaResultUnion{ + UnknownRaw: raw, + Type: MCPServerToolResultDeltaResultUnionTypeUnknown, + } +} + +func (u MCPServerToolResultDeltaResultUnion) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u MCPServerToolResultDeltaResultUnion) IsUnknown() bool { + return u.Type == MCPServerToolResultDeltaResultUnionTypeUnknown +} + +func (u *MCPServerToolResultDeltaResultUnion) UnmarshalJSON(data []byte) error { + *u = MCPServerToolResultDeltaResultUnion{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var arrayOfFunctionResultSubcontent []FunctionResultSubcontent = []FunctionResultSubcontent{} + if err := utils.UnmarshalJSON(data, &arrayOfFunctionResultSubcontent, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MCPServerToolResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent, + Value: arrayOfFunctionResultSubcontent, + }) + } + + var mcpServerToolResultDeltaResult MCPServerToolResultDeltaResult = MCPServerToolResultDeltaResult{} + if err := utils.UnmarshalJSON(data, &mcpServerToolResultDeltaResult, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MCPServerToolResultDeltaResultUnionTypeMCPServerToolResultDeltaResult, + Value: &mcpServerToolResultDeltaResult, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MCPServerToolResultDeltaResultUnionTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = MCPServerToolResultDeltaResultUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = MCPServerToolResultDeltaResultUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(MCPServerToolResultDeltaResultUnionType) + switch best.Type { + case MCPServerToolResultDeltaResultUnionTypeArrayOfFunctionResultSubcontent: + u.ArrayOfFunctionResultSubcontent = best.Value.([]FunctionResultSubcontent) + return nil + case MCPServerToolResultDeltaResultUnionTypeMCPServerToolResultDeltaResult: + u.MCPServerToolResultDeltaResult = best.Value.(*MCPServerToolResultDeltaResult) + return nil + case MCPServerToolResultDeltaResultUnionTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = MCPServerToolResultDeltaResultUnionTypeUnknown + return nil +} + +func (u MCPServerToolResultDeltaResultUnion) MarshalJSON() ([]byte, error) { + if u.ArrayOfFunctionResultSubcontent != nil { + return utils.MarshalJSON(u.ArrayOfFunctionResultSubcontent, "", true) + } + + if u.MCPServerToolResultDeltaResult != nil { + return utils.MarshalJSON(u.MCPServerToolResultDeltaResult, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type MCPServerToolResultDeltaResultUnion: all fields are null") +} + +type MCPServerToolResultDelta struct { + Name *string `json:"name,omitzero"` + Result MCPServerToolResultDeltaResultUnion `json:"result"` + ServerName *string `json:"server_name,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"mcp_server_tool_result" json:"type"` +} + +func (m MCPServerToolResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServerToolResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *MCPServerToolResultDelta) GetName() *string { + if m == nil { + return nil + } + return m.Name +} + +func (m *MCPServerToolResultDelta) GetResult() MCPServerToolResultDeltaResultUnion { + if m == nil { + return MCPServerToolResultDeltaResultUnion{} + } + return m.Result +} + +func (m *MCPServerToolResultDelta) GetServerName() *string { + if m == nil { + return nil + } + return m.ServerName +} + +func (m *MCPServerToolResultDelta) GetType() string { + return "mcp_server_tool_result" +} diff --git a/internal/sdk/models/interactions/mcpservertoolresultstep.go b/internal/sdk/models/interactions/mcpservertoolresultstep.go new file mode 100644 index 0000000..62b3a3d --- /dev/null +++ b/internal/sdk/models/interactions/mcpservertoolresultstep.go @@ -0,0 +1,238 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type MCPServerToolResultStepResult struct { +} + +func (m MCPServerToolResultStepResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServerToolResultStepResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +type MCPServerToolResultStepResultUnionType string + +const ( + MCPServerToolResultStepResultUnionTypeArrayOfFunctionResultSubcontent MCPServerToolResultStepResultUnionType = "arrayOfFunctionResultSubcontent" + MCPServerToolResultStepResultUnionTypeMCPServerToolResultStepResult MCPServerToolResultStepResultUnionType = "MCPServerToolResultStep_result" + MCPServerToolResultStepResultUnionTypeStr MCPServerToolResultStepResultUnionType = "str" + MCPServerToolResultStepResultUnionTypeUnknown MCPServerToolResultStepResultUnionType = "Unknown" +) + +// MCPServerToolResultStepResultUnion - Required. The output from the MCP server call. Can be simple text or rich content. +type MCPServerToolResultStepResultUnion struct { + ArrayOfFunctionResultSubcontent []FunctionResultSubcontent `queryParam:"inline" union:"member"` + MCPServerToolResultStepResult *MCPServerToolResultStepResult `queryParam:"inline" union:"member"` + Str *string `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type MCPServerToolResultStepResultUnionType +} + +func CreateMCPServerToolResultStepResultUnionArrayOfFunctionResultSubcontent(arrayOfFunctionResultSubcontent []FunctionResultSubcontent) MCPServerToolResultStepResultUnion { + typ := MCPServerToolResultStepResultUnionTypeArrayOfFunctionResultSubcontent + + return MCPServerToolResultStepResultUnion{ + ArrayOfFunctionResultSubcontent: arrayOfFunctionResultSubcontent, + Type: typ, + } +} + +func CreateMCPServerToolResultStepResultUnionMCPServerToolResultStepResult(mcpServerToolResultStepResult MCPServerToolResultStepResult) MCPServerToolResultStepResultUnion { + typ := MCPServerToolResultStepResultUnionTypeMCPServerToolResultStepResult + + return MCPServerToolResultStepResultUnion{ + MCPServerToolResultStepResult: &mcpServerToolResultStepResult, + Type: typ, + } +} + +func CreateMCPServerToolResultStepResultUnionStr(str string) MCPServerToolResultStepResultUnion { + typ := MCPServerToolResultStepResultUnionTypeStr + + return MCPServerToolResultStepResultUnion{ + Str: &str, + Type: typ, + } +} + +func CreateMCPServerToolResultStepResultUnionUnknown(raw json.RawMessage) MCPServerToolResultStepResultUnion { + return MCPServerToolResultStepResultUnion{ + UnknownRaw: raw, + Type: MCPServerToolResultStepResultUnionTypeUnknown, + } +} + +func (u MCPServerToolResultStepResultUnion) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u MCPServerToolResultStepResultUnion) IsUnknown() bool { + return u.Type == MCPServerToolResultStepResultUnionTypeUnknown +} + +func (u *MCPServerToolResultStepResultUnion) UnmarshalJSON(data []byte) error { + *u = MCPServerToolResultStepResultUnion{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var arrayOfFunctionResultSubcontent []FunctionResultSubcontent = []FunctionResultSubcontent{} + if err := utils.UnmarshalJSON(data, &arrayOfFunctionResultSubcontent, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MCPServerToolResultStepResultUnionTypeArrayOfFunctionResultSubcontent, + Value: arrayOfFunctionResultSubcontent, + }) + } + + var mcpServerToolResultStepResult MCPServerToolResultStepResult = MCPServerToolResultStepResult{} + if err := utils.UnmarshalJSON(data, &mcpServerToolResultStepResult, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MCPServerToolResultStepResultUnionTypeMCPServerToolResultStepResult, + Value: &mcpServerToolResultStepResult, + }) + } + + var str string = "" + if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MCPServerToolResultStepResultUnionTypeStr, + Value: &str, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = MCPServerToolResultStepResultUnionTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = MCPServerToolResultStepResultUnionTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(MCPServerToolResultStepResultUnionType) + switch best.Type { + case MCPServerToolResultStepResultUnionTypeArrayOfFunctionResultSubcontent: + u.ArrayOfFunctionResultSubcontent = best.Value.([]FunctionResultSubcontent) + return nil + case MCPServerToolResultStepResultUnionTypeMCPServerToolResultStepResult: + u.MCPServerToolResultStepResult = best.Value.(*MCPServerToolResultStepResult) + return nil + case MCPServerToolResultStepResultUnionTypeStr: + u.Str = best.Value.(*string) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = MCPServerToolResultStepResultUnionTypeUnknown + return nil +} + +func (u MCPServerToolResultStepResultUnion) MarshalJSON() ([]byte, error) { + if u.ArrayOfFunctionResultSubcontent != nil { + return utils.MarshalJSON(u.ArrayOfFunctionResultSubcontent, "", true) + } + + if u.MCPServerToolResultStepResult != nil { + return utils.MarshalJSON(u.MCPServerToolResultStepResult, "", true) + } + + if u.Str != nil { + return utils.MarshalJSON(u.Str, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type MCPServerToolResultStepResultUnion: all fields are null") +} + +// MCPServerToolResultStep - MCPServer tool result step. +type MCPServerToolResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // Name of the tool which is called for this specific tool call. + Name *string `json:"name,omitzero"` + // Required. The output from the MCP server call. Can be simple text or rich content. + Result MCPServerToolResultStepResultUnion `json:"result"` + // The name of the used MCP server. + ServerName *string `json:"server_name,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"mcp_server_tool_result" json:"type"` +} + +func (m MCPServerToolResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *MCPServerToolResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *MCPServerToolResultStep) GetCallID() string { + if m == nil { + return "" + } + return m.CallID +} + +func (m *MCPServerToolResultStep) GetName() *string { + if m == nil { + return nil + } + return m.Name +} + +func (m *MCPServerToolResultStep) GetResult() MCPServerToolResultStepResultUnion { + if m == nil { + return MCPServerToolResultStepResultUnion{} + } + return m.Result +} + +func (m *MCPServerToolResultStep) GetServerName() *string { + if m == nil { + return nil + } + return m.ServerName +} + +func (m *MCPServerToolResultStep) GetType() string { + return "mcp_server_tool_result" +} diff --git a/internal/sdk/models/interactions/mediaprocessing.go b/internal/sdk/models/interactions/mediaprocessing.go new file mode 100644 index 0000000..f2a9d6c --- /dev/null +++ b/internal/sdk/models/interactions/mediaprocessing.go @@ -0,0 +1,114 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type MediaProcessingType string + +const ( + MediaProcessingTypeStaticMediaProcessing MediaProcessingType = "StaticMediaProcessing" + MediaProcessingTypeUnknown MediaProcessingType = "Unknown" +) + +type MediaProcessing struct { + StaticMediaProcessing *StaticMediaProcessing `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type MediaProcessingType +} + +func CreateMediaProcessingStaticMediaProcessing(staticMediaProcessing StaticMediaProcessing) MediaProcessing { + typ := MediaProcessingTypeStaticMediaProcessing + + return MediaProcessing{ + StaticMediaProcessing: &staticMediaProcessing, + Type: typ, + } +} + +func CreateMediaProcessingUnknown(raw json.RawMessage) MediaProcessing { + return MediaProcessing{ + UnknownRaw: raw, + Type: MediaProcessingTypeUnknown, + } +} + +func (u MediaProcessing) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u MediaProcessing) IsUnknown() bool { + return u.Type == MediaProcessingTypeUnknown +} + +func (u *MediaProcessing) UnmarshalJSON(data []byte) error { + *u = MediaProcessing{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var staticMediaProcessing StaticMediaProcessing = StaticMediaProcessing{} + if err := utils.UnmarshalJSON(data, &staticMediaProcessing, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: MediaProcessingTypeStaticMediaProcessing, + Value: &staticMediaProcessing, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = MediaProcessingTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = MediaProcessingTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(MediaProcessingType) + switch best.Type { + case MediaProcessingTypeStaticMediaProcessing: + u.StaticMediaProcessing = best.Value.(*StaticMediaProcessing) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = MediaProcessingTypeUnknown + return nil +} + +func (u MediaProcessing) MarshalJSON() ([]byte, error) { + if u.StaticMediaProcessing != nil { + return utils.MarshalJSON(u.StaticMediaProcessing, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type MediaProcessing: all fields are null") +} diff --git a/internal/sdk/models/interactions/mediaresolution.go b/internal/sdk/models/interactions/mediaresolution.go new file mode 100644 index 0000000..cea9dd0 --- /dev/null +++ b/internal/sdk/models/interactions/mediaresolution.go @@ -0,0 +1,41 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type MediaResolution string + +const ( + MediaResolutionLow MediaResolution = "low" + MediaResolutionMedium MediaResolution = "medium" + MediaResolutionHigh MediaResolution = "high" + MediaResolutionUltraHigh MediaResolution = "ultra_high" +) + +func (e MediaResolution) ToPointer() *MediaResolution { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *MediaResolution) IsExact() bool { + if e != nil { + switch *e { + case "low", "medium", "high", "ultra_high": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/modalitytokens.go b/internal/sdk/models/interactions/modalitytokens.go new file mode 100644 index 0000000..53a5fa4 --- /dev/null +++ b/internal/sdk/models/interactions/modalitytokens.go @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ModalityTokens - The token count for a single response modality. +type ModalityTokens struct { + Modality *ResponseModality `json:"modality,omitzero"` + // Number of tokens for the modality. + Tokens *int `json:"tokens,omitzero"` +} + +func (m ModalityTokens) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModalityTokens) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModalityTokens) GetModality() *ResponseModality { + if m == nil { + return nil + } + return m.Modality +} + +func (m *ModalityTokens) GetTokens() *int { + if m == nil { + return nil + } + return m.Tokens +} diff --git a/internal/sdk/models/interactions/model.go b/internal/sdk/models/interactions/model.go new file mode 100644 index 0000000..d802cdc --- /dev/null +++ b/internal/sdk/models/interactions/model.go @@ -0,0 +1,86 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +// Model - The model that will complete your prompt.\n\nSee [models](https://ai.google.dev/gemini-api/docs/models) for additional details. +type Model string + +const ( + // ModelGemini25Flash Our first hybrid reasoning model which supports a 1M token context window and has thinking budgets. + ModelGemini25Flash Model = "gemini-2.5-flash" + // ModelGemini25Pro Our state-of-the-art multipurpose model, which excels at coding and complex reasoning tasks. + ModelGemini25Pro Model = "gemini-2.5-pro" + // ModelGemma426bA4bIt Gemma 4 26B A4B IT + ModelGemma426bA4bIt Model = "gemma-4-26b-a4b-it" + // ModelGemma431bIt Gemma 4 31B IT + ModelGemma431bIt Model = "gemma-4-31b-it" + // ModelGeminiFlashLatest Latest release of Gemini Flash + ModelGeminiFlashLatest Model = "gemini-flash-latest" + // ModelGeminiFlashLiteLatest Latest release of Gemini Flash-Lite + ModelGeminiFlashLiteLatest Model = "gemini-flash-lite-latest" + // ModelGeminiProLatest Latest release of Gemini Pro + ModelGeminiProLatest Model = "gemini-pro-latest" + // ModelGemini25FlashLite Our smallest and most cost effective model, built for at scale usage. + ModelGemini25FlashLite Model = "gemini-2.5-flash-lite" + // ModelGemini25FlashImage Our native image generation model, optimized for speed, flexibility, and contextual understanding. Text input and output is priced the same as 2.5 Flash. + ModelGemini25FlashImage Model = "gemini-2.5-flash-image" + // ModelGemini3FlashPreview Our most intelligent model built for speed, combining frontier intelligence with superior search and grounding. + ModelGemini3FlashPreview Model = "gemini-3-flash-preview" + // ModelGemini31ProPreview Our latest SOTA reasoning model with unprecedented depth and nuance, and powerful multimodal understanding and coding capabilities. + ModelGemini31ProPreview Model = "gemini-3.1-pro-preview" + // ModelGemini31ProPreviewCustomtools Gemini 3.1 Pro Preview optimized for custom tool usage + ModelGemini31ProPreviewCustomtools Model = "gemini-3.1-pro-preview-customtools" + // ModelGemini31FlashLite Our most cost-efficient model, optimized for high-volume agentic tasks, translation, and simple data processing. + ModelGemini31FlashLite Model = "gemini-3.1-flash-lite" + // ModelGemini3ProImage Gemini 3 Pro Image + ModelGemini3ProImage Model = "gemini-3-pro-image" + // ModelNanoBananaProPreview Gemini 3 Pro Image Preview + ModelNanoBananaProPreview Model = "nano-banana-pro-preview" + // ModelGemini31FlashImage Gemini 3.1 Flash Image. + ModelGemini31FlashImage Model = "gemini-3.1-flash-image" + // ModelGemini35Flash Gemini 3.5 Flash - Our earlier Flash model, built for speed and foundational performance across routine, high-throughput workloads. + ModelGemini35Flash Model = "gemini-3.5-flash" + // ModelGemini36Flash Gemini 3.6 Flash - Our previous generation Flash model, balancing speed and multimodal capabilities across general agentic and everyday tasks. + ModelGemini36Flash Model = "gemini-3.6-flash" + // ModelGemini37Flash Gemini 3.7 Flash - Our high-speed, efficient Flash model built for everyday coding, agentic tool use, and reliable multi-step execution. + ModelGemini37Flash Model = "gemini-3.7-flash" + // ModelGemini38Flash Gemini 3.8 Flash - Our most intelligent Flash model, engineered for long-horizon software engineering, autonomous agents, and complex enterprise workflows. + ModelGemini38Flash Model = "gemini-3.8-flash" + // ModelLyria3ClipPreview Our low-latency, music generation model optimized for high-fidelity audio clips and precise rhythmic control. + ModelLyria3ClipPreview Model = "lyria-3-clip-preview" + // ModelLyria3ProPreview Our advanced, full-song generative model with deep compositional understanding, optimized for precise structural control and complex transitions across diverse musical styles. + ModelLyria3ProPreview Model = "lyria-3-pro-preview" + // ModelGeminiRoboticsEr16Preview Gemini Robotics-ER 1.6 Preview + ModelGeminiRoboticsEr16Preview Model = "gemini-robotics-er-1.6-preview" + // ModelGeminiRoboticsEr2Preview Gemini Robotics Embodied Reasoning 2 Preview + ModelGeminiRoboticsEr2Preview Model = "gemini-robotics-er-2-preview" +) + +func (e Model) ToPointer() *Model { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Model) IsExact() bool { + if e != nil { + switch *e { + case "gemini-2.5-flash", "gemini-2.5-pro", "gemma-4-26b-a4b-it", "gemma-4-31b-it", "gemini-flash-latest", "gemini-flash-lite-latest", "gemini-pro-latest", "gemini-2.5-flash-lite", "gemini-2.5-flash-image", "gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.1-pro-preview-customtools", "gemini-3.1-flash-lite", "gemini-3-pro-image", "nano-banana-pro-preview", "gemini-3.1-flash-image", "gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "lyria-3-clip-preview", "lyria-3-pro-preview", "gemini-robotics-er-1.6-preview", "gemini-robotics-er-2-preview": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/modeloutputstep.go b/internal/sdk/models/interactions/modeloutputstep.go new file mode 100644 index 0000000..8fa8d37 --- /dev/null +++ b/internal/sdk/models/interactions/modeloutputstep.go @@ -0,0 +1,65 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ModelOutputStep - Output generated by the model. +type ModelOutputStep struct { + Content []Content `json:"content,omitzero"` + // The `Status` type defines a logical error model that is suitable for + // different programming environments, including REST APIs and RPC APIs. It is + // used by [gRPC](https://github.com/grpc). Each `Status` message contains + // three pieces of data: error code, error message, and error details. + // + // You can find out more about this error model and how to work with it in the + // [API Design Guide](https://cloud.google.com/apis/design/errors). + Error *Status `json:"error,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"model_output" json:"type"` +} + +func (m ModelOutputStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelOutputStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelOutputStep) GetContent() []Content { + if m == nil { + return nil + } + return m.Content +} + +func (m *ModelOutputStep) GetError() *Status { + if m == nil { + return nil + } + return m.Error +} + +func (m *ModelOutputStep) GetType() string { + return "model_output" +} diff --git a/internal/sdk/models/interactions/parallelaisearchconfig.go b/internal/sdk/models/interactions/parallelaisearchconfig.go new file mode 100644 index 0000000..7903be8 --- /dev/null +++ b/internal/sdk/models/interactions/parallelaisearchconfig.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ParallelAISearchConfig - Used to specify configuration for ParallelAISearch. +type ParallelAISearchConfig struct { + // Optional. The API key for ParallelAiSearch. + APIKey *string `json:"api_key,omitzero"` + // Optional. Custom configs for ParallelAiSearch. + CustomConfig map[string]any `json:"custom_config,omitzero"` +} + +func (p ParallelAISearchConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *ParallelAISearchConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *ParallelAISearchConfig) GetAPIKey() *string { + if p == nil { + return nil + } + return p.APIKey +} + +func (p *ParallelAISearchConfig) GetCustomConfig() map[string]any { + if p == nil { + return nil + } + return p.CustomConfig +} diff --git a/internal/sdk/models/interactions/placecitation.go b/internal/sdk/models/interactions/placecitation.go new file mode 100644 index 0000000..5d64b93 --- /dev/null +++ b/internal/sdk/models/interactions/placecitation.go @@ -0,0 +1,99 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// PlaceCitation - A place citation annotation. +type PlaceCitation struct { + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // Title of the place. + Name *string `json:"name,omitzero"` + // The ID of the place, in `places/{place_id}` format. + PlaceID *string `json:"place_id,omitzero"` + // Snippets of reviews that are used to generate answers about the + // features of a given place in Google Maps. + ReviewSnippets []ReviewSnippet `json:"review_snippets,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"place_citation" json:"type"` + // URI reference of the place. + URL *string `json:"url,omitzero"` +} + +func (p PlaceCitation) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *PlaceCitation) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *PlaceCitation) GetEndIndex() *int { + if p == nil { + return nil + } + return p.EndIndex +} + +func (p *PlaceCitation) GetName() *string { + if p == nil { + return nil + } + return p.Name +} + +func (p *PlaceCitation) GetPlaceID() *string { + if p == nil { + return nil + } + return p.PlaceID +} + +func (p *PlaceCitation) GetReviewSnippets() []ReviewSnippet { + if p == nil { + return nil + } + return p.ReviewSnippets +} + +func (p *PlaceCitation) GetStartIndex() *int { + if p == nil { + return nil + } + return p.StartIndex +} + +func (p *PlaceCitation) GetType() string { + return "place_citation" +} + +func (p *PlaceCitation) GetURL() *string { + if p == nil { + return nil + } + return p.URL +} diff --git a/internal/sdk/models/interactions/processingcalldelta.go b/internal/sdk/models/interactions/processingcalldelta.go new file mode 100644 index 0000000..95d1118 --- /dev/null +++ b/internal/sdk/models/interactions/processingcalldelta.go @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ProcessingCallDelta - Streaming delta for a server-initiated media processing step. +type ProcessingCallDelta struct { + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"processing_call" json:"type"` +} + +func (p ProcessingCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *ProcessingCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *ProcessingCallDelta) GetSignature() *string { + if p == nil { + return nil + } + return p.Signature +} + +func (p *ProcessingCallDelta) GetType() string { + return "processing_call" +} diff --git a/internal/sdk/models/interactions/processingcallstep.go b/internal/sdk/models/interactions/processingcallstep.go new file mode 100644 index 0000000..a0cf4eb --- /dev/null +++ b/internal/sdk/models/interactions/processingcallstep.go @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ProcessingCallStep - A server-initiated processing step for media analysis (e.g. video +// understanding). +type ProcessingCallStep struct { + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"processing_call" json:"type"` +} + +func (p ProcessingCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *ProcessingCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *ProcessingCallStep) GetID() string { + if p == nil { + return "" + } + return p.ID +} + +func (p *ProcessingCallStep) GetSignature() *string { + if p == nil { + return nil + } + return p.Signature +} + +func (p *ProcessingCallStep) GetType() string { + return "processing_call" +} diff --git a/internal/sdk/models/interactions/processingresultdelta.go b/internal/sdk/models/interactions/processingresultdelta.go new file mode 100644 index 0000000..a6dc615 --- /dev/null +++ b/internal/sdk/models/interactions/processingresultdelta.go @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ProcessingResultDelta - Streaming delta for the result of a server-initiated media processing step. +type ProcessingResultDelta struct { + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"processing_result" json:"type"` +} + +func (p ProcessingResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *ProcessingResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *ProcessingResultDelta) GetSignature() *string { + if p == nil { + return nil + } + return p.Signature +} + +func (p *ProcessingResultDelta) GetType() string { + return "processing_result" +} diff --git a/internal/sdk/models/interactions/processingresultstep.go b/internal/sdk/models/interactions/processingresultstep.go new file mode 100644 index 0000000..d6cc25f --- /dev/null +++ b/internal/sdk/models/interactions/processingresultstep.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ProcessingResultStep - The result of a server-initiated media processing step. +type ProcessingResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"processing_result" json:"type"` +} + +func (p ProcessingResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *ProcessingResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *ProcessingResultStep) GetCallID() string { + if p == nil { + return "" + } + return p.CallID +} + +func (p *ProcessingResultStep) GetSignature() *string { + if p == nil { + return nil + } + return p.Signature +} + +func (p *ProcessingResultStep) GetType() string { + return "processing_result" +} diff --git a/internal/sdk/models/interactions/ragresource.go b/internal/sdk/models/interactions/ragresource.go new file mode 100644 index 0000000..10e0ae4 --- /dev/null +++ b/internal/sdk/models/interactions/ragresource.go @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RagResource - The definition of the Rag resource. +type RagResource struct { + // Optional. RagCorpora resource name. + RagCorpus *string `json:"rag_corpus,omitzero"` + // Optional. rag_file_id. The files should be in the same rag_corpus set in + // rag_corpus field. + RagFileIds []string `json:"rag_file_ids,omitzero"` +} + +func (r RagResource) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RagResource) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RagResource) GetRagCorpus() *string { + if r == nil { + return nil + } + return r.RagCorpus +} + +func (r *RagResource) GetRagFileIds() []string { + if r == nil { + return nil + } + return r.RagFileIds +} diff --git a/internal/sdk/models/interactions/ragretrievalconfig.go b/internal/sdk/models/interactions/ragretrievalconfig.go new file mode 100644 index 0000000..f2a3f61 --- /dev/null +++ b/internal/sdk/models/interactions/ragretrievalconfig.go @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RagRetrievalConfig - Specifies the context retrieval config. +type RagRetrievalConfig struct { + // Config for filters. + Filter *Filter `json:"filter,omitzero"` + // Config for Hybrid Search. + HybridSearch *HybridSearch `json:"hybrid_search,omitzero"` + // Config for Rank Service. + Ranking *Ranking `json:"ranking,omitzero"` + // Optional. The number of contexts to retrieve. + TopK *int `json:"top_k,omitzero"` +} + +func (r RagRetrievalConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RagRetrievalConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RagRetrievalConfig) GetFilter() *Filter { + if r == nil { + return nil + } + return r.Filter +} + +func (r *RagRetrievalConfig) GetHybridSearch() *HybridSearch { + if r == nil { + return nil + } + return r.HybridSearch +} + +func (r *RagRetrievalConfig) GetRanking() *Ranking { + if r == nil { + return nil + } + return r.Ranking +} + +func (r *RagRetrievalConfig) GetTopK() *int { + if r == nil { + return nil + } + return r.TopK +} diff --git a/internal/sdk/models/interactions/ragstoreconfig.go b/internal/sdk/models/interactions/ragstoreconfig.go new file mode 100644 index 0000000..aaba124 --- /dev/null +++ b/internal/sdk/models/interactions/ragstoreconfig.go @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RagStoreConfig - Use to specify configuration for RAG Store. +type RagStoreConfig struct { + // Optional. The representation of the rag source. + RagResources []RagResource `json:"rag_resources,omitzero"` + // Specifies the context retrieval config. + RagRetrievalConfig *RagRetrievalConfig `json:"rag_retrieval_config,omitzero"` + // Optional. Number of top k results to return from the selected corpora. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + SimilarityTopK *int `json:"similarity_top_k,omitzero"` + // Optional. Only return results with vector distance smaller than the threshold. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + VectorDistanceThreshold *float64 `json:"vector_distance_threshold,omitzero"` +} + +func (r RagStoreConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RagStoreConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RagStoreConfig) GetRagResources() []RagResource { + if r == nil { + return nil + } + return r.RagResources +} + +func (r *RagStoreConfig) GetRagRetrievalConfig() *RagRetrievalConfig { + if r == nil { + return nil + } + return r.RagRetrievalConfig +} + +func (r *RagStoreConfig) GetSimilarityTopK() *int { + if r == nil { + return nil + } + return r.SimilarityTopK +} + +func (r *RagStoreConfig) GetVectorDistanceThreshold() *float64 { + if r == nil { + return nil + } + return r.VectorDistanceThreshold +} diff --git a/internal/sdk/models/interactions/ranking.go b/internal/sdk/models/interactions/ranking.go new file mode 100644 index 0000000..56cd8e5 --- /dev/null +++ b/internal/sdk/models/interactions/ranking.go @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Ranking - Config for Rank Service. +type Ranking struct { + // Optional. The model name of the rank service. + ModelName *string `json:"model_name,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + rankingConfig string `const:"rank_service" json:"ranking_config"` +} + +func (r Ranking) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *Ranking) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *Ranking) GetModelName() *string { + if r == nil { + return nil + } + return r.ModelName +} + +func (r *Ranking) GetRankingConfig() string { + return "rank_service" +} diff --git a/internal/sdk/models/interactions/responseformat.go b/internal/sdk/models/interactions/responseformat.go new file mode 100644 index 0000000..b2955b3 --- /dev/null +++ b/internal/sdk/models/interactions/responseformat.go @@ -0,0 +1,218 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ResponseFormatType string + +const ( + ResponseFormatTypeAudioResponseFormat ResponseFormatType = "AudioResponseFormat" + ResponseFormatTypeImageResponseFormat ResponseFormatType = "ImageResponseFormat" + ResponseFormatTypeTextResponseFormat ResponseFormatType = "TextResponseFormat" + ResponseFormatTypeVideoResponseFormat ResponseFormatType = "VideoResponseFormat" + ResponseFormatTypeMapOfAny ResponseFormatType = "mapOfAny" + ResponseFormatTypeUnknown ResponseFormatType = "Unknown" +) + +type ResponseFormat struct { + AudioResponseFormat *AudioResponseFormat `queryParam:"inline" union:"member"` + ImageResponseFormat *ImageResponseFormat `queryParam:"inline" union:"member"` + TextResponseFormat *TextResponseFormat `queryParam:"inline" union:"member"` + VideoResponseFormat *VideoResponseFormat `queryParam:"inline" union:"member"` + MapOfAny map[string]any `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type ResponseFormatType +} + +func CreateResponseFormatAudioResponseFormat(audioResponseFormat AudioResponseFormat) ResponseFormat { + typ := ResponseFormatTypeAudioResponseFormat + + return ResponseFormat{ + AudioResponseFormat: &audioResponseFormat, + Type: typ, + } +} + +func CreateResponseFormatImageResponseFormat(imageResponseFormat ImageResponseFormat) ResponseFormat { + typ := ResponseFormatTypeImageResponseFormat + + return ResponseFormat{ + ImageResponseFormat: &imageResponseFormat, + Type: typ, + } +} + +func CreateResponseFormatTextResponseFormat(textResponseFormat TextResponseFormat) ResponseFormat { + typ := ResponseFormatTypeTextResponseFormat + + return ResponseFormat{ + TextResponseFormat: &textResponseFormat, + Type: typ, + } +} + +func CreateResponseFormatVideoResponseFormat(videoResponseFormat VideoResponseFormat) ResponseFormat { + typ := ResponseFormatTypeVideoResponseFormat + + return ResponseFormat{ + VideoResponseFormat: &videoResponseFormat, + Type: typ, + } +} + +func CreateResponseFormatMapOfAny(mapOfAny map[string]any) ResponseFormat { + typ := ResponseFormatTypeMapOfAny + + return ResponseFormat{ + MapOfAny: mapOfAny, + Type: typ, + } +} + +func CreateResponseFormatUnknown(raw json.RawMessage) ResponseFormat { + return ResponseFormat{ + UnknownRaw: raw, + Type: ResponseFormatTypeUnknown, + } +} + +func (u ResponseFormat) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u ResponseFormat) IsUnknown() bool { + return u.Type == ResponseFormatTypeUnknown +} + +func (u *ResponseFormat) UnmarshalJSON(data []byte) error { + *u = ResponseFormat{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var audioResponseFormat AudioResponseFormat = AudioResponseFormat{} + if err := utils.UnmarshalJSON(data, &audioResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ResponseFormatTypeAudioResponseFormat, + Value: &audioResponseFormat, + }) + } + + var imageResponseFormat ImageResponseFormat = ImageResponseFormat{} + if err := utils.UnmarshalJSON(data, &imageResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ResponseFormatTypeImageResponseFormat, + Value: &imageResponseFormat, + }) + } + + var textResponseFormat TextResponseFormat = TextResponseFormat{} + if err := utils.UnmarshalJSON(data, &textResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ResponseFormatTypeTextResponseFormat, + Value: &textResponseFormat, + }) + } + + var videoResponseFormat VideoResponseFormat = VideoResponseFormat{} + if err := utils.UnmarshalJSON(data, &videoResponseFormat, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ResponseFormatTypeVideoResponseFormat, + Value: &videoResponseFormat, + }) + } + + var mapOfAny map[string]any = map[string]any{} + if err := utils.UnmarshalJSON(data, &mapOfAny, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ResponseFormatTypeMapOfAny, + Value: mapOfAny, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = ResponseFormatTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ResponseFormatTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(ResponseFormatType) + switch best.Type { + case ResponseFormatTypeAudioResponseFormat: + u.AudioResponseFormat = best.Value.(*AudioResponseFormat) + return nil + case ResponseFormatTypeImageResponseFormat: + u.ImageResponseFormat = best.Value.(*ImageResponseFormat) + return nil + case ResponseFormatTypeTextResponseFormat: + u.TextResponseFormat = best.Value.(*TextResponseFormat) + return nil + case ResponseFormatTypeVideoResponseFormat: + u.VideoResponseFormat = best.Value.(*VideoResponseFormat) + return nil + case ResponseFormatTypeMapOfAny: + u.MapOfAny = best.Value.(map[string]any) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = ResponseFormatTypeUnknown + return nil +} + +func (u ResponseFormat) MarshalJSON() ([]byte, error) { + if u.AudioResponseFormat != nil { + return utils.MarshalJSON(u.AudioResponseFormat, "", true) + } + + if u.ImageResponseFormat != nil { + return utils.MarshalJSON(u.ImageResponseFormat, "", true) + } + + if u.TextResponseFormat != nil { + return utils.MarshalJSON(u.TextResponseFormat, "", true) + } + + if u.VideoResponseFormat != nil { + return utils.MarshalJSON(u.VideoResponseFormat, "", true) + } + + if u.MapOfAny != nil { + return utils.MarshalJSON(u.MapOfAny, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type ResponseFormat: all fields are null") +} diff --git a/internal/sdk/models/interactions/responsemodality.go b/internal/sdk/models/interactions/responsemodality.go new file mode 100644 index 0000000..f3559ae --- /dev/null +++ b/internal/sdk/models/interactions/responsemodality.go @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type ResponseModality string + +const ( + ResponseModalityText ResponseModality = "text" + ResponseModalityImage ResponseModality = "image" + ResponseModalityAudio ResponseModality = "audio" + ResponseModalityVideo ResponseModality = "video" + ResponseModalityDocument ResponseModality = "document" +) + +func (e ResponseModality) ToPointer() *ResponseModality { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ResponseModality) IsExact() bool { + if e != nil { + switch *e { + case "text", "image", "audio", "video", "document": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/retrieval.go b/internal/sdk/models/interactions/retrieval.go new file mode 100644 index 0000000..fee204d --- /dev/null +++ b/internal/sdk/models/interactions/retrieval.go @@ -0,0 +1,111 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type RetrievalRetrievalType string + +const ( + RetrievalRetrievalTypeVertexAiSearch RetrievalRetrievalType = "vertex_ai_search" + RetrievalRetrievalTypeRagStore RetrievalRetrievalType = "rag_store" + RetrievalRetrievalTypeExaAiSearch RetrievalRetrievalType = "exa_ai_search" + RetrievalRetrievalTypeParallelAiSearch RetrievalRetrievalType = "parallel_ai_search" +) + +func (e RetrievalRetrievalType) ToPointer() *RetrievalRetrievalType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *RetrievalRetrievalType) IsExact() bool { + if e != nil { + switch *e { + case "vertex_ai_search", "rag_store", "exa_ai_search", "parallel_ai_search": + return true + } + } + return false +} + +// Retrieval - A tool that can be used by the model to retrieve files. +type Retrieval struct { + // Used to specify configuration for ExaAISearch. + ExaAiSearchConfig *ExaAISearchConfig `json:"exa_ai_search_config,omitzero"` + // Used to specify configuration for ParallelAISearch. + ParallelAiSearchConfig *ParallelAISearchConfig `json:"parallel_ai_search_config,omitzero"` + // Use to specify configuration for RAG Store. + RagStoreConfig *RagStoreConfig `json:"rag_store_config,omitzero"` + // The types of file retrieval to enable. + RetrievalTypes []RetrievalRetrievalType `json:"retrieval_types,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"retrieval" json:"type"` + // Used to specify configuration for VertexAISearch. + VertexAiSearchConfig *VertexAISearchConfig `json:"vertex_ai_search_config,omitzero"` +} + +func (r Retrieval) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *Retrieval) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *Retrieval) GetExaAiSearchConfig() *ExaAISearchConfig { + if r == nil { + return nil + } + return r.ExaAiSearchConfig +} + +func (r *Retrieval) GetParallelAiSearchConfig() *ParallelAISearchConfig { + if r == nil { + return nil + } + return r.ParallelAiSearchConfig +} + +func (r *Retrieval) GetRagStoreConfig() *RagStoreConfig { + if r == nil { + return nil + } + return r.RagStoreConfig +} + +func (r *Retrieval) GetRetrievalTypes() []RetrievalRetrievalType { + if r == nil { + return nil + } + return r.RetrievalTypes +} + +func (r *Retrieval) GetType() string { + return "retrieval" +} + +func (r *Retrieval) GetVertexAiSearchConfig() *VertexAISearchConfig { + if r == nil { + return nil + } + return r.VertexAiSearchConfig +} diff --git a/internal/sdk/models/interactions/retrievalcallarguments.go b/internal/sdk/models/interactions/retrievalcallarguments.go new file mode 100644 index 0000000..e1e2e9b --- /dev/null +++ b/internal/sdk/models/interactions/retrievalcallarguments.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RetrievalCallArguments - The arguments to pass to Retrieval tools. +type RetrievalCallArguments struct { + // Queries for Retrieval information. + Queries []string `json:"queries,omitzero"` +} + +func (r RetrievalCallArguments) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RetrievalCallArguments) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RetrievalCallArguments) GetQueries() []string { + if r == nil { + return nil + } + return r.Queries +} diff --git a/internal/sdk/models/interactions/retrievalcalldelta.go b/internal/sdk/models/interactions/retrievalcalldelta.go new file mode 100644 index 0000000..3d2b5ac --- /dev/null +++ b/internal/sdk/models/interactions/retrievalcalldelta.go @@ -0,0 +1,95 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RetrievalCallDeltaRetrievalType - The type of retrieval tools. +type RetrievalCallDeltaRetrievalType string + +const ( + RetrievalCallDeltaRetrievalTypeVertexAiSearch RetrievalCallDeltaRetrievalType = "vertex_ai_search" + RetrievalCallDeltaRetrievalTypeRagStore RetrievalCallDeltaRetrievalType = "rag_store" + RetrievalCallDeltaRetrievalTypeExaAiSearch RetrievalCallDeltaRetrievalType = "exa_ai_search" + RetrievalCallDeltaRetrievalTypeParallelAiSearch RetrievalCallDeltaRetrievalType = "parallel_ai_search" +) + +func (e RetrievalCallDeltaRetrievalType) ToPointer() *RetrievalCallDeltaRetrievalType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *RetrievalCallDeltaRetrievalType) IsExact() bool { + if e != nil { + switch *e { + case "vertex_ai_search", "rag_store", "exa_ai_search", "parallel_ai_search": + return true + } + } + return false +} + +// RetrievalCallDelta - Used by Vertex Retrieval tools such as Parallel AI, Exa AI, Vertex AI Search, +// etc. RetrievalType decides which tool is used. +type RetrievalCallDelta struct { + // The arguments to pass to Retrieval tools. + Arguments RetrievalCallArguments `json:"arguments"` + // The type of retrieval tools. + RetrievalType *RetrievalCallDeltaRetrievalType `json:"retrieval_type,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"retrieval_call" json:"type"` +} + +func (r RetrievalCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RetrievalCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RetrievalCallDelta) GetArguments() RetrievalCallArguments { + if r == nil { + return RetrievalCallArguments{} + } + return r.Arguments +} + +func (r *RetrievalCallDelta) GetRetrievalType() *RetrievalCallDeltaRetrievalType { + if r == nil { + return nil + } + return r.RetrievalType +} + +func (r *RetrievalCallDelta) GetSignature() *string { + if r == nil { + return nil + } + return r.Signature +} + +func (r *RetrievalCallDelta) GetType() string { + return "retrieval_call" +} diff --git a/internal/sdk/models/interactions/retrievalcallstep.go b/internal/sdk/models/interactions/retrievalcallstep.go new file mode 100644 index 0000000..43a4020 --- /dev/null +++ b/internal/sdk/models/interactions/retrievalcallstep.go @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RetrievalCallStepRetrievalType - The type of retrieval tools. +type RetrievalCallStepRetrievalType string + +const ( + RetrievalCallStepRetrievalTypeVertexAiSearch RetrievalCallStepRetrievalType = "vertex_ai_search" + RetrievalCallStepRetrievalTypeRagStore RetrievalCallStepRetrievalType = "rag_store" + RetrievalCallStepRetrievalTypeExaAiSearch RetrievalCallStepRetrievalType = "exa_ai_search" + RetrievalCallStepRetrievalTypeParallelAiSearch RetrievalCallStepRetrievalType = "parallel_ai_search" +) + +func (e RetrievalCallStepRetrievalType) ToPointer() *RetrievalCallStepRetrievalType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *RetrievalCallStepRetrievalType) IsExact() bool { + if e != nil { + switch *e { + case "vertex_ai_search", "rag_store", "exa_ai_search", "parallel_ai_search": + return true + } + } + return false +} + +// RetrievalCallStep - Retrieval call step. +// Used by Vertex Retrieval tools such as Parallel AI, Exa AI, Vertex AI Search, +// etc. RetrievalType decides which tool is used. +type RetrievalCallStep struct { + // The arguments to pass to Retrieval tools. + Arguments RetrievalCallArguments `json:"arguments"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // The type of retrieval tools. + RetrievalType *RetrievalCallStepRetrievalType `json:"retrieval_type,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"retrieval_call" json:"type"` +} + +func (r RetrievalCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RetrievalCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RetrievalCallStep) GetArguments() RetrievalCallArguments { + if r == nil { + return RetrievalCallArguments{} + } + return r.Arguments +} + +func (r *RetrievalCallStep) GetID() string { + if r == nil { + return "" + } + return r.ID +} + +func (r *RetrievalCallStep) GetRetrievalType() *RetrievalCallStepRetrievalType { + if r == nil { + return nil + } + return r.RetrievalType +} + +func (r *RetrievalCallStep) GetSignature() *string { + if r == nil { + return nil + } + return r.Signature +} + +func (r *RetrievalCallStep) GetType() string { + return "retrieval_call" +} diff --git a/internal/sdk/models/interactions/retrievalresultdelta.go b/internal/sdk/models/interactions/retrievalresultdelta.go new file mode 100644 index 0000000..77b0af3 --- /dev/null +++ b/internal/sdk/models/interactions/retrievalresultdelta.go @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RetrievalResultDelta - Used by Vertex Retrieval tools such as Parallel AI, Exa AI, Vertex AI Search, +// etc. +// ToolResultDelta.type +type RetrievalResultDelta struct { + // Whether the retrieval resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"retrieval_result" json:"type"` +} + +func (r RetrievalResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RetrievalResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RetrievalResultDelta) GetIsError() *bool { + if r == nil { + return nil + } + return r.IsError +} + +func (r *RetrievalResultDelta) GetSignature() *string { + if r == nil { + return nil + } + return r.Signature +} + +func (r *RetrievalResultDelta) GetType() string { + return "retrieval_result" +} diff --git a/internal/sdk/models/interactions/retrievalresultstep.go b/internal/sdk/models/interactions/retrievalresultstep.go new file mode 100644 index 0000000..01b3859 --- /dev/null +++ b/internal/sdk/models/interactions/retrievalresultstep.go @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// RetrievalResultStep - Vertex Retrieval result step. +// Used by Vertex Retrieval tools such as Parallel AI, Exa AI, Vertex AI Search, +// etc. +type RetrievalResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // Whether the retrieval resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"retrieval_result" json:"type"` +} + +func (r RetrievalResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RetrievalResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RetrievalResultStep) GetCallID() string { + if r == nil { + return "" + } + return r.CallID +} + +func (r *RetrievalResultStep) GetIsError() *bool { + if r == nil { + return nil + } + return r.IsError +} + +func (r *RetrievalResultStep) GetSignature() *string { + if r == nil { + return nil + } + return r.Signature +} + +func (r *RetrievalResultStep) GetType() string { + return "retrieval_result" +} diff --git a/internal/sdk/models/interactions/reviewsnippet.go b/internal/sdk/models/interactions/reviewsnippet.go new file mode 100644 index 0000000..8ec920e --- /dev/null +++ b/internal/sdk/models/interactions/reviewsnippet.go @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ReviewSnippet - Encapsulates a snippet of a user review that answers a question about +// the features of a specific place in Google Maps. +type ReviewSnippet struct { + // The ID of the review snippet. + ReviewID *string `json:"review_id,omitzero"` + // Title of the review. + Title *string `json:"title,omitzero"` + // A link that corresponds to the user review on Google Maps. + URL *string `json:"url,omitzero"` +} + +func (r ReviewSnippet) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *ReviewSnippet) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *ReviewSnippet) GetReviewID() *string { + if r == nil { + return nil + } + return r.ReviewID +} + +func (r *ReviewSnippet) GetTitle() *string { + if r == nil { + return nil + } + return r.Title +} + +func (r *ReviewSnippet) GetURL() *string { + if r == nil { + return nil + } + return r.URL +} diff --git a/internal/sdk/models/interactions/safetysetting.go b/internal/sdk/models/interactions/safetysetting.go new file mode 100644 index 0000000..a19d365 --- /dev/null +++ b/internal/sdk/models/interactions/safetysetting.go @@ -0,0 +1,120 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Method - Optional. The method for blocking content. If not specified, the default +// behavior is to use the probability score. +type Method string + +const ( + MethodSeverity Method = "severity" + MethodProbability Method = "probability" +) + +func (e Method) ToPointer() *Method { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Method) IsExact() bool { + if e != nil { + switch *e { + case "severity", "probability": + return true + } + } + return false +} + +// Threshold - Required. The threshold for blocking content. If the harm probability +// exceeds this threshold, the content will be blocked. +type Threshold string + +const ( + ThresholdBlockLowAndAbove Threshold = "block_low_and_above" + ThresholdBlockMediumAndAbove Threshold = "block_medium_and_above" + ThresholdBlockOnlyHigh Threshold = "block_only_high" + ThresholdBlockNone Threshold = "block_none" + ThresholdOff Threshold = "off" +) + +func (e Threshold) ToPointer() *Threshold { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Threshold) IsExact() bool { + if e != nil { + switch *e { + case "block_low_and_above", "block_medium_and_above", "block_only_high", "block_none", "off": + return true + } + } + return false +} + +// SafetySetting - A safety setting that affects the safety-blocking behavior. +// +// A SafetySetting consists of a +// harm category and a +// threshold for that +// category. +type SafetySetting struct { + // Optional. The method for blocking content. If not specified, the default + // behavior is to use the probability score. + Method *Method `json:"method,omitzero"` + // Required. The threshold for blocking content. If the harm probability + // exceeds this threshold, the content will be blocked. + Threshold Threshold `json:"threshold"` + Type HarmCategory `json:"type"` +} + +func (s SafetySetting) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *SafetySetting) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *SafetySetting) GetMethod() *Method { + if s == nil { + return nil + } + return s.Method +} + +func (s *SafetySetting) GetThreshold() Threshold { + if s == nil { + return Threshold("") + } + return s.Threshold +} + +func (s *SafetySetting) GetType() HarmCategory { + if s == nil { + return HarmCategory("") + } + return s.Type +} diff --git a/internal/sdk/models/interactions/servicetier.go b/internal/sdk/models/interactions/servicetier.go new file mode 100644 index 0000000..30204e8 --- /dev/null +++ b/internal/sdk/models/interactions/servicetier.go @@ -0,0 +1,41 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type ServiceTier string + +const ( + ServiceTierFlex ServiceTier = "flex" + ServiceTierStandard ServiceTier = "standard" + ServiceTierPriority ServiceTier = "priority" + ServiceTierDeferred ServiceTier = "deferred" +) + +func (e ServiceTier) ToPointer() *ServiceTier { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ServiceTier) IsExact() bool { + if e != nil { + switch *e { + case "flex", "standard", "priority", "deferred": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/sessionconfig.go b/internal/sdk/models/interactions/sessionconfig.go new file mode 100644 index 0000000..7a94670 --- /dev/null +++ b/internal/sdk/models/interactions/sessionconfig.go @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// SessionConfig - The configuration of CodeMender sessions. +type SessionConfig struct { + // The maximum number of interaction rounds the agent is allowed to perform + // before reaching a timeout. + MaxRounds *int `json:"max_rounds,omitzero"` +} + +func (s SessionConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *SessionConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *SessionConfig) GetMaxRounds() *int { + if s == nil { + return nil + } + return s.MaxRounds +} diff --git a/internal/sdk/models/interactions/smarttranscriptionmode.go b/internal/sdk/models/interactions/smarttranscriptionmode.go new file mode 100644 index 0000000..0db972a --- /dev/null +++ b/internal/sdk/models/interactions/smarttranscriptionmode.go @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// SmartTranscriptionMode - Configuration for smart transcription mode. +type SmartTranscriptionMode struct { + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"smart" json:"type"` +} + +func (s SmartTranscriptionMode) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *SmartTranscriptionMode) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *SmartTranscriptionMode) GetType() string { + return "smart" +} diff --git a/internal/sdk/models/interactions/source.go b/internal/sdk/models/interactions/source.go new file mode 100644 index 0000000..66dce80 --- /dev/null +++ b/internal/sdk/models/interactions/source.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type SourceType string + +const ( + SourceTypeGcs SourceType = "gcs" + SourceTypeInline SourceType = "inline" + SourceTypeRepository SourceType = "repository" + SourceTypeSkillRegistry SourceType = "skill_registry" +) + +func (e SourceType) ToPointer() *SourceType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *SourceType) IsExact() bool { + if e != nil { + switch *e { + case "gcs", "inline", "repository", "skill_registry": + return true + } + } + return false +} + +// Source - A source to be mounted into the environment. +type Source struct { + // The inline content if `type` is `INLINE`. + Content *string `json:"content,omitzero"` + // Optional encoding for inline content (e.g. `base64`). + Encoding *string `json:"encoding,omitzero"` + // The source of the environment. + // For Cloud Storage, this is the Cloud Storage path. + // For GitHub, this is the GitHub path. + Source *string `json:"source,omitzero"` + // Where the source should appear in the environment. + Target *string `json:"target,omitzero"` + Type *SourceType `json:"type,omitzero"` +} + +func (s Source) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *Source) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *Source) GetContent() *string { + if s == nil { + return nil + } + return s.Content +} + +func (s *Source) GetEncoding() *string { + if s == nil { + return nil + } + return s.Encoding +} + +func (s *Source) GetSource() *string { + if s == nil { + return nil + } + return s.Source +} + +func (s *Source) GetTarget() *string { + if s == nil { + return nil + } + return s.Target +} + +func (s *Source) GetType() *SourceType { + if s == nil { + return nil + } + return s.Type +} diff --git a/internal/sdk/models/interactions/speakerconfig.go b/internal/sdk/models/interactions/speakerconfig.go new file mode 100644 index 0000000..4ded220 --- /dev/null +++ b/internal/sdk/models/interactions/speakerconfig.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// SpeakerConfig - Configuration for multi-speaker and speech generation. +type SpeakerConfig struct { + // Individual speaker configurations. + Speakers []SpeechConfig `json:"speakers,omitzero"` +} + +func (s SpeakerConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *SpeakerConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *SpeakerConfig) GetSpeakers() []SpeechConfig { + if s == nil { + return nil + } + return s.Speakers +} diff --git a/internal/sdk/models/interactions/speechconfig.go b/internal/sdk/models/interactions/speechconfig.go new file mode 100644 index 0000000..f21fd8f --- /dev/null +++ b/internal/sdk/models/interactions/speechconfig.go @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// SpeechConfig - The configuration for speech interaction. +type SpeechConfig struct { + // The language of the speech. + Language *string `json:"language,omitzero"` + // The speaker's name, it should match the speaker name given in the prompt. + Speaker *string `json:"speaker,omitzero"` + // The voice of the speaker. + Voice *string `json:"voice,omitzero"` +} + +func (s SpeechConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *SpeechConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *SpeechConfig) GetLanguage() *string { + if s == nil { + return nil + } + return s.Language +} + +func (s *SpeechConfig) GetSpeaker() *string { + if s == nil { + return nil + } + return s.Speaker +} + +func (s *SpeechConfig) GetVoice() *string { + if s == nil { + return nil + } + return s.Voice +} diff --git a/internal/sdk/models/interactions/staticmediaprocessing.go b/internal/sdk/models/interactions/staticmediaprocessing.go new file mode 100644 index 0000000..1cf7d81 --- /dev/null +++ b/internal/sdk/models/interactions/staticmediaprocessing.go @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StaticMediaProcessing struct { + // Optional. Segment end time. Specified as a decimal number of seconds followed + // by an 's' suffix, e.g., "30s". Must be non-negative and greater than + // `start_offset` if `start_offset` is set. + EndOffset *string `json:"end_offset,omitzero"` + // Optional. Video frame-rate sampling density. + Fps *float64 `json:"fps,omitzero"` + // Optional. Segment start time. Specified as a decimal number of seconds followed + // by an 's' suffix, e.g., "10.5s". Must be non-negative. + StartOffset *string `json:"start_offset,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"static" json:"type"` +} + +func (s StaticMediaProcessing) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StaticMediaProcessing) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StaticMediaProcessing) GetEndOffset() *string { + if s == nil { + return nil + } + return s.EndOffset +} + +func (s *StaticMediaProcessing) GetFps() *float64 { + if s == nil { + return nil + } + return s.Fps +} + +func (s *StaticMediaProcessing) GetStartOffset() *string { + if s == nil { + return nil + } + return s.StartOffset +} + +func (s *StaticMediaProcessing) GetType() string { + return "static" +} diff --git a/internal/sdk/models/interactions/status.go b/internal/sdk/models/interactions/status.go new file mode 100644 index 0000000..081ae9b --- /dev/null +++ b/internal/sdk/models/interactions/status.go @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Status - The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +type Status struct { + // The status code, which should be an enum value of google.rpc.Code. + Code *int `json:"code,omitzero"` + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + Details []map[string]any `json:"details,omitzero"` + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // google.rpc.Status.details field, or localized by the client. + Message *string `json:"message,omitzero"` +} + +func (s Status) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *Status) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *Status) GetCode() *int { + if s == nil { + return nil + } + return s.Code +} + +func (s *Status) GetDetails() []map[string]any { + if s == nil { + return nil + } + return s.Details +} + +func (s *Status) GetMessage() *string { + if s == nil { + return nil + } + return s.Message +} diff --git a/internal/sdk/models/interactions/step.go b/internal/sdk/models/interactions/step.go new file mode 100644 index 0000000..452cd33 --- /dev/null +++ b/internal/sdk/models/interactions/step.go @@ -0,0 +1,598 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StepType string + +const ( + StepTypeCodeExecutionCall StepType = "code_execution_call" + StepTypeCodeExecutionResult StepType = "code_execution_result" + StepTypeFileSearchCall StepType = "file_search_call" + StepTypeFileSearchResult StepType = "file_search_result" + StepTypeFunctionCall StepType = "function_call" + StepTypeFunctionResult StepType = "function_result" + StepTypeGoogleMapsCall StepType = "google_maps_call" + StepTypeGoogleMapsResult StepType = "google_maps_result" + StepTypeGoogleSearchCall StepType = "google_search_call" + StepTypeGoogleSearchResult StepType = "google_search_result" + StepTypeMcpServerToolCall StepType = "mcp_server_tool_call" + StepTypeMcpServerToolResult StepType = "mcp_server_tool_result" + StepTypeModelOutput StepType = "model_output" + StepTypeProcessingCall StepType = "processing_call" + StepTypeProcessingResult StepType = "processing_result" + StepTypeRetrievalCall StepType = "retrieval_call" + StepTypeRetrievalResult StepType = "retrieval_result" + StepTypeThought StepType = "thought" + StepTypeURLContextCall StepType = "url_context_call" + StepTypeURLContextResult StepType = "url_context_result" + StepTypeUserInput StepType = "user_input" + StepTypeUnknown StepType = "UNKNOWN" +) + +// Step - A step in the interaction. +type Step struct { + CodeExecutionCallStep *CodeExecutionCallStep `queryParam:"inline" union:"member"` + CodeExecutionResultStep *CodeExecutionResultStep `queryParam:"inline" union:"member"` + FileSearchCallStep *FileSearchCallStep `queryParam:"inline" union:"member"` + FileSearchResultStep *FileSearchResultStep `queryParam:"inline" union:"member"` + FunctionCallStep *FunctionCallStep `queryParam:"inline" union:"member"` + FunctionResultStep *FunctionResultStep `queryParam:"inline" union:"member"` + GoogleMapsCallStep *GoogleMapsCallStep `queryParam:"inline" union:"member"` + GoogleMapsResultStep *GoogleMapsResultStep `queryParam:"inline" union:"member"` + GoogleSearchCallStep *GoogleSearchCallStep `queryParam:"inline" union:"member"` + GoogleSearchResultStep *GoogleSearchResultStep `queryParam:"inline" union:"member"` + MCPServerToolCallStep *MCPServerToolCallStep `queryParam:"inline" union:"member"` + MCPServerToolResultStep *MCPServerToolResultStep `queryParam:"inline" union:"member"` + ModelOutputStep *ModelOutputStep `queryParam:"inline" union:"member"` + ProcessingCallStep *ProcessingCallStep `queryParam:"inline" union:"member"` + ProcessingResultStep *ProcessingResultStep `queryParam:"inline" union:"member"` + RetrievalCallStep *RetrievalCallStep `queryParam:"inline" union:"member"` + RetrievalResultStep *RetrievalResultStep `queryParam:"inline" union:"member"` + ThoughtStep *ThoughtStep `queryParam:"inline" union:"member"` + URLContextCallStep *URLContextCallStep `queryParam:"inline" union:"member"` + URLContextResultStep *URLContextResultStep `queryParam:"inline" union:"member"` + UserInputStep *UserInputStep `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type StepType +} + +func CreateStepCodeExecutionCall(codeExecutionCall CodeExecutionCallStep) Step { + typ := StepTypeCodeExecutionCall + + return Step{ + CodeExecutionCallStep: &codeExecutionCall, + Type: typ, + } +} + +func CreateStepCodeExecutionResult(codeExecutionResult CodeExecutionResultStep) Step { + typ := StepTypeCodeExecutionResult + + return Step{ + CodeExecutionResultStep: &codeExecutionResult, + Type: typ, + } +} + +func CreateStepFileSearchCall(fileSearchCall FileSearchCallStep) Step { + typ := StepTypeFileSearchCall + + return Step{ + FileSearchCallStep: &fileSearchCall, + Type: typ, + } +} + +func CreateStepFileSearchResult(fileSearchResult FileSearchResultStep) Step { + typ := StepTypeFileSearchResult + + return Step{ + FileSearchResultStep: &fileSearchResult, + Type: typ, + } +} + +func CreateStepFunctionCall(functionCall FunctionCallStep) Step { + typ := StepTypeFunctionCall + + return Step{ + FunctionCallStep: &functionCall, + Type: typ, + } +} + +func CreateStepFunctionResult(functionResult FunctionResultStep) Step { + typ := StepTypeFunctionResult + + return Step{ + FunctionResultStep: &functionResult, + Type: typ, + } +} + +func CreateStepGoogleMapsCall(googleMapsCall GoogleMapsCallStep) Step { + typ := StepTypeGoogleMapsCall + + return Step{ + GoogleMapsCallStep: &googleMapsCall, + Type: typ, + } +} + +func CreateStepGoogleMapsResult(googleMapsResult GoogleMapsResultStep) Step { + typ := StepTypeGoogleMapsResult + + return Step{ + GoogleMapsResultStep: &googleMapsResult, + Type: typ, + } +} + +func CreateStepGoogleSearchCall(googleSearchCall GoogleSearchCallStep) Step { + typ := StepTypeGoogleSearchCall + + return Step{ + GoogleSearchCallStep: &googleSearchCall, + Type: typ, + } +} + +func CreateStepGoogleSearchResult(googleSearchResult GoogleSearchResultStep) Step { + typ := StepTypeGoogleSearchResult + + return Step{ + GoogleSearchResultStep: &googleSearchResult, + Type: typ, + } +} + +func CreateStepMcpServerToolCall(mcpServerToolCall MCPServerToolCallStep) Step { + typ := StepTypeMcpServerToolCall + + return Step{ + MCPServerToolCallStep: &mcpServerToolCall, + Type: typ, + } +} + +func CreateStepMcpServerToolResult(mcpServerToolResult MCPServerToolResultStep) Step { + typ := StepTypeMcpServerToolResult + + return Step{ + MCPServerToolResultStep: &mcpServerToolResult, + Type: typ, + } +} + +func CreateStepModelOutput(modelOutput ModelOutputStep) Step { + typ := StepTypeModelOutput + + return Step{ + ModelOutputStep: &modelOutput, + Type: typ, + } +} + +func CreateStepProcessingCall(processingCall ProcessingCallStep) Step { + typ := StepTypeProcessingCall + + return Step{ + ProcessingCallStep: &processingCall, + Type: typ, + } +} + +func CreateStepProcessingResult(processingResult ProcessingResultStep) Step { + typ := StepTypeProcessingResult + + return Step{ + ProcessingResultStep: &processingResult, + Type: typ, + } +} + +func CreateStepRetrievalCall(retrievalCall RetrievalCallStep) Step { + typ := StepTypeRetrievalCall + + return Step{ + RetrievalCallStep: &retrievalCall, + Type: typ, + } +} + +func CreateStepRetrievalResult(retrievalResult RetrievalResultStep) Step { + typ := StepTypeRetrievalResult + + return Step{ + RetrievalResultStep: &retrievalResult, + Type: typ, + } +} + +func CreateStepThought(thought ThoughtStep) Step { + typ := StepTypeThought + + return Step{ + ThoughtStep: &thought, + Type: typ, + } +} + +func CreateStepURLContextCall(urlContextCall URLContextCallStep) Step { + typ := StepTypeURLContextCall + + return Step{ + URLContextCallStep: &urlContextCall, + Type: typ, + } +} + +func CreateStepURLContextResult(urlContextResult URLContextResultStep) Step { + typ := StepTypeURLContextResult + + return Step{ + URLContextResultStep: &urlContextResult, + Type: typ, + } +} + +func CreateStepUserInput(userInput UserInputStep) Step { + typ := StepTypeUserInput + + return Step{ + UserInputStep: &userInput, + Type: typ, + } +} + +func CreateStepUnknown(raw json.RawMessage) Step { + return Step{ + UnknownRaw: raw, + Type: StepTypeUnknown, + } +} + +func (u Step) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Step) IsUnknown() bool { + return u.Type == StepTypeUnknown +} + +func (u *Step) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = Step{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = StepTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = StepTypeUnknown + return nil + } + + switch dis.Type { + case "code_execution_call": + codeExecutionCallStep := new(CodeExecutionCallStep) + if err := utils.UnmarshalJSON(data, &codeExecutionCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code_execution_call) type CodeExecutionCallStep within Step: %w", string(data), err) + } + + u.CodeExecutionCallStep = codeExecutionCallStep + u.Type = StepTypeCodeExecutionCall + return nil + case "code_execution_result": + codeExecutionResultStep := new(CodeExecutionResultStep) + if err := utils.UnmarshalJSON(data, &codeExecutionResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code_execution_result) type CodeExecutionResultStep within Step: %w", string(data), err) + } + + u.CodeExecutionResultStep = codeExecutionResultStep + u.Type = StepTypeCodeExecutionResult + return nil + case "file_search_call": + fileSearchCallStep := new(FileSearchCallStep) + if err := utils.UnmarshalJSON(data, &fileSearchCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == file_search_call) type FileSearchCallStep within Step: %w", string(data), err) + } + + u.FileSearchCallStep = fileSearchCallStep + u.Type = StepTypeFileSearchCall + return nil + case "file_search_result": + fileSearchResultStep := new(FileSearchResultStep) + if err := utils.UnmarshalJSON(data, &fileSearchResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == file_search_result) type FileSearchResultStep within Step: %w", string(data), err) + } + + u.FileSearchResultStep = fileSearchResultStep + u.Type = StepTypeFileSearchResult + return nil + case "function_call": + functionCallStep := new(FunctionCallStep) + if err := utils.UnmarshalJSON(data, &functionCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == function_call) type FunctionCallStep within Step: %w", string(data), err) + } + + u.FunctionCallStep = functionCallStep + u.Type = StepTypeFunctionCall + return nil + case "function_result": + functionResultStep := new(FunctionResultStep) + if err := utils.UnmarshalJSON(data, &functionResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == function_result) type FunctionResultStep within Step: %w", string(data), err) + } + + u.FunctionResultStep = functionResultStep + u.Type = StepTypeFunctionResult + return nil + case "google_maps_call": + googleMapsCallStep := new(GoogleMapsCallStep) + if err := utils.UnmarshalJSON(data, &googleMapsCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_maps_call) type GoogleMapsCallStep within Step: %w", string(data), err) + } + + u.GoogleMapsCallStep = googleMapsCallStep + u.Type = StepTypeGoogleMapsCall + return nil + case "google_maps_result": + googleMapsResultStep := new(GoogleMapsResultStep) + if err := utils.UnmarshalJSON(data, &googleMapsResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_maps_result) type GoogleMapsResultStep within Step: %w", string(data), err) + } + + u.GoogleMapsResultStep = googleMapsResultStep + u.Type = StepTypeGoogleMapsResult + return nil + case "google_search_call": + googleSearchCallStep := new(GoogleSearchCallStep) + if err := utils.UnmarshalJSON(data, &googleSearchCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_search_call) type GoogleSearchCallStep within Step: %w", string(data), err) + } + + u.GoogleSearchCallStep = googleSearchCallStep + u.Type = StepTypeGoogleSearchCall + return nil + case "google_search_result": + googleSearchResultStep := new(GoogleSearchResultStep) + if err := utils.UnmarshalJSON(data, &googleSearchResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_search_result) type GoogleSearchResultStep within Step: %w", string(data), err) + } + + u.GoogleSearchResultStep = googleSearchResultStep + u.Type = StepTypeGoogleSearchResult + return nil + case "mcp_server_tool_call": + mcpServerToolCallStep := new(MCPServerToolCallStep) + if err := utils.UnmarshalJSON(data, &mcpServerToolCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == mcp_server_tool_call) type MCPServerToolCallStep within Step: %w", string(data), err) + } + + u.MCPServerToolCallStep = mcpServerToolCallStep + u.Type = StepTypeMcpServerToolCall + return nil + case "mcp_server_tool_result": + mcpServerToolResultStep := new(MCPServerToolResultStep) + if err := utils.UnmarshalJSON(data, &mcpServerToolResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == mcp_server_tool_result) type MCPServerToolResultStep within Step: %w", string(data), err) + } + + u.MCPServerToolResultStep = mcpServerToolResultStep + u.Type = StepTypeMcpServerToolResult + return nil + case "model_output": + modelOutputStep := new(ModelOutputStep) + if err := utils.UnmarshalJSON(data, &modelOutputStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == model_output) type ModelOutputStep within Step: %w", string(data), err) + } + + u.ModelOutputStep = modelOutputStep + u.Type = StepTypeModelOutput + return nil + case "processing_call": + processingCallStep := new(ProcessingCallStep) + if err := utils.UnmarshalJSON(data, &processingCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == processing_call) type ProcessingCallStep within Step: %w", string(data), err) + } + + u.ProcessingCallStep = processingCallStep + u.Type = StepTypeProcessingCall + return nil + case "processing_result": + processingResultStep := new(ProcessingResultStep) + if err := utils.UnmarshalJSON(data, &processingResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == processing_result) type ProcessingResultStep within Step: %w", string(data), err) + } + + u.ProcessingResultStep = processingResultStep + u.Type = StepTypeProcessingResult + return nil + case "retrieval_call": + retrievalCallStep := new(RetrievalCallStep) + if err := utils.UnmarshalJSON(data, &retrievalCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == retrieval_call) type RetrievalCallStep within Step: %w", string(data), err) + } + + u.RetrievalCallStep = retrievalCallStep + u.Type = StepTypeRetrievalCall + return nil + case "retrieval_result": + retrievalResultStep := new(RetrievalResultStep) + if err := utils.UnmarshalJSON(data, &retrievalResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == retrieval_result) type RetrievalResultStep within Step: %w", string(data), err) + } + + u.RetrievalResultStep = retrievalResultStep + u.Type = StepTypeRetrievalResult + return nil + case "thought": + thoughtStep := new(ThoughtStep) + if err := utils.UnmarshalJSON(data, &thoughtStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == thought) type ThoughtStep within Step: %w", string(data), err) + } + + u.ThoughtStep = thoughtStep + u.Type = StepTypeThought + return nil + case "url_context_call": + urlContextCallStep := new(URLContextCallStep) + if err := utils.UnmarshalJSON(data, &urlContextCallStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_context_call) type URLContextCallStep within Step: %w", string(data), err) + } + + u.URLContextCallStep = urlContextCallStep + u.Type = StepTypeURLContextCall + return nil + case "url_context_result": + urlContextResultStep := new(URLContextResultStep) + if err := utils.UnmarshalJSON(data, &urlContextResultStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_context_result) type URLContextResultStep within Step: %w", string(data), err) + } + + u.URLContextResultStep = urlContextResultStep + u.Type = StepTypeURLContextResult + return nil + case "user_input": + userInputStep := new(UserInputStep) + if err := utils.UnmarshalJSON(data, &userInputStep, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == user_input) type UserInputStep within Step: %w", string(data), err) + } + + u.UserInputStep = userInputStep + u.Type = StepTypeUserInput + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = StepTypeUnknown + return nil + } + +} + +func (u Step) MarshalJSON() ([]byte, error) { + if u.CodeExecutionCallStep != nil { + return utils.MarshalJSON(u.CodeExecutionCallStep, "", true) + } + + if u.CodeExecutionResultStep != nil { + return utils.MarshalJSON(u.CodeExecutionResultStep, "", true) + } + + if u.FileSearchCallStep != nil { + return utils.MarshalJSON(u.FileSearchCallStep, "", true) + } + + if u.FileSearchResultStep != nil { + return utils.MarshalJSON(u.FileSearchResultStep, "", true) + } + + if u.FunctionCallStep != nil { + return utils.MarshalJSON(u.FunctionCallStep, "", true) + } + + if u.FunctionResultStep != nil { + return utils.MarshalJSON(u.FunctionResultStep, "", true) + } + + if u.GoogleMapsCallStep != nil { + return utils.MarshalJSON(u.GoogleMapsCallStep, "", true) + } + + if u.GoogleMapsResultStep != nil { + return utils.MarshalJSON(u.GoogleMapsResultStep, "", true) + } + + if u.GoogleSearchCallStep != nil { + return utils.MarshalJSON(u.GoogleSearchCallStep, "", true) + } + + if u.GoogleSearchResultStep != nil { + return utils.MarshalJSON(u.GoogleSearchResultStep, "", true) + } + + if u.MCPServerToolCallStep != nil { + return utils.MarshalJSON(u.MCPServerToolCallStep, "", true) + } + + if u.MCPServerToolResultStep != nil { + return utils.MarshalJSON(u.MCPServerToolResultStep, "", true) + } + + if u.ModelOutputStep != nil { + return utils.MarshalJSON(u.ModelOutputStep, "", true) + } + + if u.ProcessingCallStep != nil { + return utils.MarshalJSON(u.ProcessingCallStep, "", true) + } + + if u.ProcessingResultStep != nil { + return utils.MarshalJSON(u.ProcessingResultStep, "", true) + } + + if u.RetrievalCallStep != nil { + return utils.MarshalJSON(u.RetrievalCallStep, "", true) + } + + if u.RetrievalResultStep != nil { + return utils.MarshalJSON(u.RetrievalResultStep, "", true) + } + + if u.ThoughtStep != nil { + return utils.MarshalJSON(u.ThoughtStep, "", true) + } + + if u.URLContextCallStep != nil { + return utils.MarshalJSON(u.URLContextCallStep, "", true) + } + + if u.URLContextResultStep != nil { + return utils.MarshalJSON(u.URLContextResultStep, "", true) + } + + if u.UserInputStep != nil { + return utils.MarshalJSON(u.UserInputStep, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Step: all fields are null") +} diff --git a/internal/sdk/models/interactions/stepdelta.go b/internal/sdk/models/interactions/stepdelta.go new file mode 100644 index 0000000..660925c --- /dev/null +++ b/internal/sdk/models/interactions/stepdelta.go @@ -0,0 +1,180 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StepDelta struct { + Delta StepDeltaData `json:"delta"` + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"step.delta" json:"event_type"` + Index int `json:"index"` + // Optional metadata accompanying ANY streamed event. + Metadata *StepDeltaMetadata `json:"metadata,omitzero"` +} + +func (s StepDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StepDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StepDelta) GetDelta() StepDeltaData { + if s == nil { + return StepDeltaData{} + } + return s.Delta +} + +func (s *StepDelta) GetDeltaArgumentsDelta() *ArgumentsDelta { + return s.GetDelta().ArgumentsDelta +} + +func (s *StepDelta) GetDeltaAudio() *AudioDelta { + return s.GetDelta().AudioDelta +} + +func (s *StepDelta) GetDeltaCodeExecutionCall() *CodeExecutionCallDelta { + return s.GetDelta().CodeExecutionCallDelta +} + +func (s *StepDelta) GetDeltaCodeExecutionResult() *CodeExecutionResultDelta { + return s.GetDelta().CodeExecutionResultDelta +} + +func (s *StepDelta) GetDeltaDocument() *DocumentDelta { + return s.GetDelta().DocumentDelta +} + +func (s *StepDelta) GetDeltaFileSearchCall() *FileSearchCallDelta { + return s.GetDelta().FileSearchCallDelta +} + +func (s *StepDelta) GetDeltaFileSearchResult() *FileSearchResultDelta { + return s.GetDelta().FileSearchResultDelta +} + +func (s *StepDelta) GetDeltaFunctionResult() *FunctionResultDelta { + return s.GetDelta().FunctionResultDelta +} + +func (s *StepDelta) GetDeltaGoogleMapsCall() *GoogleMapsCallDelta { + return s.GetDelta().GoogleMapsCallDelta +} + +func (s *StepDelta) GetDeltaGoogleMapsResult() *GoogleMapsResultDelta { + return s.GetDelta().GoogleMapsResultDelta +} + +func (s *StepDelta) GetDeltaGoogleSearchCall() *GoogleSearchCallDelta { + return s.GetDelta().GoogleSearchCallDelta +} + +func (s *StepDelta) GetDeltaGoogleSearchResult() *GoogleSearchResultDelta { + return s.GetDelta().GoogleSearchResultDelta +} + +func (s *StepDelta) GetDeltaImage() *ImageDelta { + return s.GetDelta().ImageDelta +} + +func (s *StepDelta) GetDeltaMcpServerToolCall() *MCPServerToolCallDelta { + return s.GetDelta().MCPServerToolCallDelta +} + +func (s *StepDelta) GetDeltaMcpServerToolResult() *MCPServerToolResultDelta { + return s.GetDelta().MCPServerToolResultDelta +} + +func (s *StepDelta) GetDeltaProcessingCall() *ProcessingCallDelta { + return s.GetDelta().ProcessingCallDelta +} + +func (s *StepDelta) GetDeltaProcessingResult() *ProcessingResultDelta { + return s.GetDelta().ProcessingResultDelta +} + +func (s *StepDelta) GetDeltaRetrievalCall() *RetrievalCallDelta { + return s.GetDelta().RetrievalCallDelta +} + +func (s *StepDelta) GetDeltaRetrievalResult() *RetrievalResultDelta { + return s.GetDelta().RetrievalResultDelta +} + +func (s *StepDelta) GetDeltaTextAnnotationDelta() *TextAnnotationDelta { + return s.GetDelta().TextAnnotationDelta +} + +func (s *StepDelta) GetDeltaText() *TextDelta { + return s.GetDelta().TextDelta +} + +func (s *StepDelta) GetDeltaThoughtSignature() *ThoughtSignatureDelta { + return s.GetDelta().ThoughtSignatureDelta +} + +func (s *StepDelta) GetDeltaThoughtSummary() *ThoughtSummaryDelta { + return s.GetDelta().ThoughtSummaryDelta +} + +func (s *StepDelta) GetDeltaURLContextCall() *URLContextCallDelta { + return s.GetDelta().URLContextCallDelta +} + +func (s *StepDelta) GetDeltaURLContextResult() *URLContextResultDelta { + return s.GetDelta().URLContextResultDelta +} + +func (s *StepDelta) GetDeltaVideo() *VideoDelta { + return s.GetDelta().VideoDelta +} + +func (s *StepDelta) GetEventID() *string { + if s == nil { + return nil + } + return s.EventID +} + +func (s *StepDelta) GetEventType() string { + return "step.delta" +} + +func (s *StepDelta) GetIndex() int { + if s == nil { + return 0 + } + return s.Index +} + +func (s *StepDelta) GetMetadata() *StepDeltaMetadata { + if s == nil { + return nil + } + return s.Metadata +} diff --git a/internal/sdk/models/interactions/stepdeltadata.go b/internal/sdk/models/interactions/stepdeltadata.go new file mode 100644 index 0000000..d370670 --- /dev/null +++ b/internal/sdk/models/interactions/stepdeltadata.go @@ -0,0 +1,717 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StepDeltaDataType string + +const ( + StepDeltaDataTypeArgumentsDelta StepDeltaDataType = "arguments_delta" + StepDeltaDataTypeAudio StepDeltaDataType = "audio" + StepDeltaDataTypeCodeExecutionCall StepDeltaDataType = "code_execution_call" + StepDeltaDataTypeCodeExecutionResult StepDeltaDataType = "code_execution_result" + StepDeltaDataTypeDocument StepDeltaDataType = "document" + StepDeltaDataTypeFileSearchCall StepDeltaDataType = "file_search_call" + StepDeltaDataTypeFileSearchResult StepDeltaDataType = "file_search_result" + StepDeltaDataTypeFunctionResult StepDeltaDataType = "function_result" + StepDeltaDataTypeGoogleMapsCall StepDeltaDataType = "google_maps_call" + StepDeltaDataTypeGoogleMapsResult StepDeltaDataType = "google_maps_result" + StepDeltaDataTypeGoogleSearchCall StepDeltaDataType = "google_search_call" + StepDeltaDataTypeGoogleSearchResult StepDeltaDataType = "google_search_result" + StepDeltaDataTypeImage StepDeltaDataType = "image" + StepDeltaDataTypeMcpServerToolCall StepDeltaDataType = "mcp_server_tool_call" + StepDeltaDataTypeMcpServerToolResult StepDeltaDataType = "mcp_server_tool_result" + StepDeltaDataTypeProcessingCall StepDeltaDataType = "processing_call" + StepDeltaDataTypeProcessingResult StepDeltaDataType = "processing_result" + StepDeltaDataTypeRetrievalCall StepDeltaDataType = "retrieval_call" + StepDeltaDataTypeRetrievalResult StepDeltaDataType = "retrieval_result" + StepDeltaDataTypeTextAnnotationDelta StepDeltaDataType = "text_annotation_delta" + StepDeltaDataTypeText StepDeltaDataType = "text" + StepDeltaDataTypeThoughtSignature StepDeltaDataType = "thought_signature" + StepDeltaDataTypeThoughtSummary StepDeltaDataType = "thought_summary" + StepDeltaDataTypeURLContextCall StepDeltaDataType = "url_context_call" + StepDeltaDataTypeURLContextResult StepDeltaDataType = "url_context_result" + StepDeltaDataTypeVideo StepDeltaDataType = "video" + StepDeltaDataTypeUnknown StepDeltaDataType = "UNKNOWN" +) + +type StepDeltaData struct { + ArgumentsDelta *ArgumentsDelta `queryParam:"inline" union:"member"` + AudioDelta *AudioDelta `queryParam:"inline" union:"member"` + CodeExecutionCallDelta *CodeExecutionCallDelta `queryParam:"inline" union:"member"` + CodeExecutionResultDelta *CodeExecutionResultDelta `queryParam:"inline" union:"member"` + DocumentDelta *DocumentDelta `queryParam:"inline" union:"member"` + FileSearchCallDelta *FileSearchCallDelta `queryParam:"inline" union:"member"` + FileSearchResultDelta *FileSearchResultDelta `queryParam:"inline" union:"member"` + FunctionResultDelta *FunctionResultDelta `queryParam:"inline" union:"member"` + GoogleMapsCallDelta *GoogleMapsCallDelta `queryParam:"inline" union:"member"` + GoogleMapsResultDelta *GoogleMapsResultDelta `queryParam:"inline" union:"member"` + GoogleSearchCallDelta *GoogleSearchCallDelta `queryParam:"inline" union:"member"` + GoogleSearchResultDelta *GoogleSearchResultDelta `queryParam:"inline" union:"member"` + ImageDelta *ImageDelta `queryParam:"inline" union:"member"` + MCPServerToolCallDelta *MCPServerToolCallDelta `queryParam:"inline" union:"member"` + MCPServerToolResultDelta *MCPServerToolResultDelta `queryParam:"inline" union:"member"` + ProcessingCallDelta *ProcessingCallDelta `queryParam:"inline" union:"member"` + ProcessingResultDelta *ProcessingResultDelta `queryParam:"inline" union:"member"` + RetrievalCallDelta *RetrievalCallDelta `queryParam:"inline" union:"member"` + RetrievalResultDelta *RetrievalResultDelta `queryParam:"inline" union:"member"` + TextAnnotationDelta *TextAnnotationDelta `queryParam:"inline" union:"member"` + TextDelta *TextDelta `queryParam:"inline" union:"member"` + ThoughtSignatureDelta *ThoughtSignatureDelta `queryParam:"inline" union:"member"` + ThoughtSummaryDelta *ThoughtSummaryDelta `queryParam:"inline" union:"member"` + URLContextCallDelta *URLContextCallDelta `queryParam:"inline" union:"member"` + URLContextResultDelta *URLContextResultDelta `queryParam:"inline" union:"member"` + VideoDelta *VideoDelta `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type StepDeltaDataType +} + +func CreateStepDeltaDataArgumentsDelta(argumentsDelta ArgumentsDelta) StepDeltaData { + typ := StepDeltaDataTypeArgumentsDelta + + return StepDeltaData{ + ArgumentsDelta: &argumentsDelta, + Type: typ, + } +} + +func CreateStepDeltaDataAudio(audio AudioDelta) StepDeltaData { + typ := StepDeltaDataTypeAudio + + return StepDeltaData{ + AudioDelta: &audio, + Type: typ, + } +} + +func CreateStepDeltaDataCodeExecutionCall(codeExecutionCall CodeExecutionCallDelta) StepDeltaData { + typ := StepDeltaDataTypeCodeExecutionCall + + return StepDeltaData{ + CodeExecutionCallDelta: &codeExecutionCall, + Type: typ, + } +} + +func CreateStepDeltaDataCodeExecutionResult(codeExecutionResult CodeExecutionResultDelta) StepDeltaData { + typ := StepDeltaDataTypeCodeExecutionResult + + return StepDeltaData{ + CodeExecutionResultDelta: &codeExecutionResult, + Type: typ, + } +} + +func CreateStepDeltaDataDocument(document DocumentDelta) StepDeltaData { + typ := StepDeltaDataTypeDocument + + return StepDeltaData{ + DocumentDelta: &document, + Type: typ, + } +} + +func CreateStepDeltaDataFileSearchCall(fileSearchCall FileSearchCallDelta) StepDeltaData { + typ := StepDeltaDataTypeFileSearchCall + + return StepDeltaData{ + FileSearchCallDelta: &fileSearchCall, + Type: typ, + } +} + +func CreateStepDeltaDataFileSearchResult(fileSearchResult FileSearchResultDelta) StepDeltaData { + typ := StepDeltaDataTypeFileSearchResult + + return StepDeltaData{ + FileSearchResultDelta: &fileSearchResult, + Type: typ, + } +} + +func CreateStepDeltaDataFunctionResult(functionResult FunctionResultDelta) StepDeltaData { + typ := StepDeltaDataTypeFunctionResult + + return StepDeltaData{ + FunctionResultDelta: &functionResult, + Type: typ, + } +} + +func CreateStepDeltaDataGoogleMapsCall(googleMapsCall GoogleMapsCallDelta) StepDeltaData { + typ := StepDeltaDataTypeGoogleMapsCall + + return StepDeltaData{ + GoogleMapsCallDelta: &googleMapsCall, + Type: typ, + } +} + +func CreateStepDeltaDataGoogleMapsResult(googleMapsResult GoogleMapsResultDelta) StepDeltaData { + typ := StepDeltaDataTypeGoogleMapsResult + + return StepDeltaData{ + GoogleMapsResultDelta: &googleMapsResult, + Type: typ, + } +} + +func CreateStepDeltaDataGoogleSearchCall(googleSearchCall GoogleSearchCallDelta) StepDeltaData { + typ := StepDeltaDataTypeGoogleSearchCall + + return StepDeltaData{ + GoogleSearchCallDelta: &googleSearchCall, + Type: typ, + } +} + +func CreateStepDeltaDataGoogleSearchResult(googleSearchResult GoogleSearchResultDelta) StepDeltaData { + typ := StepDeltaDataTypeGoogleSearchResult + + return StepDeltaData{ + GoogleSearchResultDelta: &googleSearchResult, + Type: typ, + } +} + +func CreateStepDeltaDataImage(image ImageDelta) StepDeltaData { + typ := StepDeltaDataTypeImage + + return StepDeltaData{ + ImageDelta: &image, + Type: typ, + } +} + +func CreateStepDeltaDataMcpServerToolCall(mcpServerToolCall MCPServerToolCallDelta) StepDeltaData { + typ := StepDeltaDataTypeMcpServerToolCall + + return StepDeltaData{ + MCPServerToolCallDelta: &mcpServerToolCall, + Type: typ, + } +} + +func CreateStepDeltaDataMcpServerToolResult(mcpServerToolResult MCPServerToolResultDelta) StepDeltaData { + typ := StepDeltaDataTypeMcpServerToolResult + + return StepDeltaData{ + MCPServerToolResultDelta: &mcpServerToolResult, + Type: typ, + } +} + +func CreateStepDeltaDataProcessingCall(processingCall ProcessingCallDelta) StepDeltaData { + typ := StepDeltaDataTypeProcessingCall + + return StepDeltaData{ + ProcessingCallDelta: &processingCall, + Type: typ, + } +} + +func CreateStepDeltaDataProcessingResult(processingResult ProcessingResultDelta) StepDeltaData { + typ := StepDeltaDataTypeProcessingResult + + return StepDeltaData{ + ProcessingResultDelta: &processingResult, + Type: typ, + } +} + +func CreateStepDeltaDataRetrievalCall(retrievalCall RetrievalCallDelta) StepDeltaData { + typ := StepDeltaDataTypeRetrievalCall + + return StepDeltaData{ + RetrievalCallDelta: &retrievalCall, + Type: typ, + } +} + +func CreateStepDeltaDataRetrievalResult(retrievalResult RetrievalResultDelta) StepDeltaData { + typ := StepDeltaDataTypeRetrievalResult + + return StepDeltaData{ + RetrievalResultDelta: &retrievalResult, + Type: typ, + } +} + +func CreateStepDeltaDataTextAnnotationDelta(textAnnotationDelta TextAnnotationDelta) StepDeltaData { + typ := StepDeltaDataTypeTextAnnotationDelta + + return StepDeltaData{ + TextAnnotationDelta: &textAnnotationDelta, + Type: typ, + } +} + +func CreateStepDeltaDataText(text TextDelta) StepDeltaData { + typ := StepDeltaDataTypeText + + return StepDeltaData{ + TextDelta: &text, + Type: typ, + } +} + +func CreateStepDeltaDataThoughtSignature(thoughtSignature ThoughtSignatureDelta) StepDeltaData { + typ := StepDeltaDataTypeThoughtSignature + + return StepDeltaData{ + ThoughtSignatureDelta: &thoughtSignature, + Type: typ, + } +} + +func CreateStepDeltaDataThoughtSummary(thoughtSummary ThoughtSummaryDelta) StepDeltaData { + typ := StepDeltaDataTypeThoughtSummary + + return StepDeltaData{ + ThoughtSummaryDelta: &thoughtSummary, + Type: typ, + } +} + +func CreateStepDeltaDataURLContextCall(urlContextCall URLContextCallDelta) StepDeltaData { + typ := StepDeltaDataTypeURLContextCall + + return StepDeltaData{ + URLContextCallDelta: &urlContextCall, + Type: typ, + } +} + +func CreateStepDeltaDataURLContextResult(urlContextResult URLContextResultDelta) StepDeltaData { + typ := StepDeltaDataTypeURLContextResult + + return StepDeltaData{ + URLContextResultDelta: &urlContextResult, + Type: typ, + } +} + +func CreateStepDeltaDataVideo(video VideoDelta) StepDeltaData { + typ := StepDeltaDataTypeVideo + + return StepDeltaData{ + VideoDelta: &video, + Type: typ, + } +} + +func CreateStepDeltaDataUnknown(raw json.RawMessage) StepDeltaData { + return StepDeltaData{ + UnknownRaw: raw, + Type: StepDeltaDataTypeUnknown, + } +} + +func (u StepDeltaData) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u StepDeltaData) IsUnknown() bool { + return u.Type == StepDeltaDataTypeUnknown +} + +func (u *StepDeltaData) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = StepDeltaData{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = StepDeltaDataTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = StepDeltaDataTypeUnknown + return nil + } + + switch dis.Type { + case "arguments_delta": + argumentsDelta := new(ArgumentsDelta) + if err := utils.UnmarshalJSON(data, &argumentsDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == arguments_delta) type ArgumentsDelta within StepDeltaData: %w", string(data), err) + } + + u.ArgumentsDelta = argumentsDelta + u.Type = StepDeltaDataTypeArgumentsDelta + return nil + case "audio": + audioDelta := new(AudioDelta) + if err := utils.UnmarshalJSON(data, &audioDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == audio) type AudioDelta within StepDeltaData: %w", string(data), err) + } + + u.AudioDelta = audioDelta + u.Type = StepDeltaDataTypeAudio + return nil + case "code_execution_call": + codeExecutionCallDelta := new(CodeExecutionCallDelta) + if err := utils.UnmarshalJSON(data, &codeExecutionCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code_execution_call) type CodeExecutionCallDelta within StepDeltaData: %w", string(data), err) + } + + u.CodeExecutionCallDelta = codeExecutionCallDelta + u.Type = StepDeltaDataTypeCodeExecutionCall + return nil + case "code_execution_result": + codeExecutionResultDelta := new(CodeExecutionResultDelta) + if err := utils.UnmarshalJSON(data, &codeExecutionResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code_execution_result) type CodeExecutionResultDelta within StepDeltaData: %w", string(data), err) + } + + u.CodeExecutionResultDelta = codeExecutionResultDelta + u.Type = StepDeltaDataTypeCodeExecutionResult + return nil + case "document": + documentDelta := new(DocumentDelta) + if err := utils.UnmarshalJSON(data, &documentDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == document) type DocumentDelta within StepDeltaData: %w", string(data), err) + } + + u.DocumentDelta = documentDelta + u.Type = StepDeltaDataTypeDocument + return nil + case "file_search_call": + fileSearchCallDelta := new(FileSearchCallDelta) + if err := utils.UnmarshalJSON(data, &fileSearchCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == file_search_call) type FileSearchCallDelta within StepDeltaData: %w", string(data), err) + } + + u.FileSearchCallDelta = fileSearchCallDelta + u.Type = StepDeltaDataTypeFileSearchCall + return nil + case "file_search_result": + fileSearchResultDelta := new(FileSearchResultDelta) + if err := utils.UnmarshalJSON(data, &fileSearchResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == file_search_result) type FileSearchResultDelta within StepDeltaData: %w", string(data), err) + } + + u.FileSearchResultDelta = fileSearchResultDelta + u.Type = StepDeltaDataTypeFileSearchResult + return nil + case "function_result": + functionResultDelta := new(FunctionResultDelta) + if err := utils.UnmarshalJSON(data, &functionResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == function_result) type FunctionResultDelta within StepDeltaData: %w", string(data), err) + } + + u.FunctionResultDelta = functionResultDelta + u.Type = StepDeltaDataTypeFunctionResult + return nil + case "google_maps_call": + googleMapsCallDelta := new(GoogleMapsCallDelta) + if err := utils.UnmarshalJSON(data, &googleMapsCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_maps_call) type GoogleMapsCallDelta within StepDeltaData: %w", string(data), err) + } + + u.GoogleMapsCallDelta = googleMapsCallDelta + u.Type = StepDeltaDataTypeGoogleMapsCall + return nil + case "google_maps_result": + googleMapsResultDelta := new(GoogleMapsResultDelta) + if err := utils.UnmarshalJSON(data, &googleMapsResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_maps_result) type GoogleMapsResultDelta within StepDeltaData: %w", string(data), err) + } + + u.GoogleMapsResultDelta = googleMapsResultDelta + u.Type = StepDeltaDataTypeGoogleMapsResult + return nil + case "google_search_call": + googleSearchCallDelta := new(GoogleSearchCallDelta) + if err := utils.UnmarshalJSON(data, &googleSearchCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_search_call) type GoogleSearchCallDelta within StepDeltaData: %w", string(data), err) + } + + u.GoogleSearchCallDelta = googleSearchCallDelta + u.Type = StepDeltaDataTypeGoogleSearchCall + return nil + case "google_search_result": + googleSearchResultDelta := new(GoogleSearchResultDelta) + if err := utils.UnmarshalJSON(data, &googleSearchResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_search_result) type GoogleSearchResultDelta within StepDeltaData: %w", string(data), err) + } + + u.GoogleSearchResultDelta = googleSearchResultDelta + u.Type = StepDeltaDataTypeGoogleSearchResult + return nil + case "image": + imageDelta := new(ImageDelta) + if err := utils.UnmarshalJSON(data, &imageDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == image) type ImageDelta within StepDeltaData: %w", string(data), err) + } + + u.ImageDelta = imageDelta + u.Type = StepDeltaDataTypeImage + return nil + case "mcp_server_tool_call": + mcpServerToolCallDelta := new(MCPServerToolCallDelta) + if err := utils.UnmarshalJSON(data, &mcpServerToolCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == mcp_server_tool_call) type MCPServerToolCallDelta within StepDeltaData: %w", string(data), err) + } + + u.MCPServerToolCallDelta = mcpServerToolCallDelta + u.Type = StepDeltaDataTypeMcpServerToolCall + return nil + case "mcp_server_tool_result": + mcpServerToolResultDelta := new(MCPServerToolResultDelta) + if err := utils.UnmarshalJSON(data, &mcpServerToolResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == mcp_server_tool_result) type MCPServerToolResultDelta within StepDeltaData: %w", string(data), err) + } + + u.MCPServerToolResultDelta = mcpServerToolResultDelta + u.Type = StepDeltaDataTypeMcpServerToolResult + return nil + case "processing_call": + processingCallDelta := new(ProcessingCallDelta) + if err := utils.UnmarshalJSON(data, &processingCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == processing_call) type ProcessingCallDelta within StepDeltaData: %w", string(data), err) + } + + u.ProcessingCallDelta = processingCallDelta + u.Type = StepDeltaDataTypeProcessingCall + return nil + case "processing_result": + processingResultDelta := new(ProcessingResultDelta) + if err := utils.UnmarshalJSON(data, &processingResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == processing_result) type ProcessingResultDelta within StepDeltaData: %w", string(data), err) + } + + u.ProcessingResultDelta = processingResultDelta + u.Type = StepDeltaDataTypeProcessingResult + return nil + case "retrieval_call": + retrievalCallDelta := new(RetrievalCallDelta) + if err := utils.UnmarshalJSON(data, &retrievalCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == retrieval_call) type RetrievalCallDelta within StepDeltaData: %w", string(data), err) + } + + u.RetrievalCallDelta = retrievalCallDelta + u.Type = StepDeltaDataTypeRetrievalCall + return nil + case "retrieval_result": + retrievalResultDelta := new(RetrievalResultDelta) + if err := utils.UnmarshalJSON(data, &retrievalResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == retrieval_result) type RetrievalResultDelta within StepDeltaData: %w", string(data), err) + } + + u.RetrievalResultDelta = retrievalResultDelta + u.Type = StepDeltaDataTypeRetrievalResult + return nil + case "text_annotation_delta": + textAnnotationDelta := new(TextAnnotationDelta) + if err := utils.UnmarshalJSON(data, &textAnnotationDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == text_annotation_delta) type TextAnnotationDelta within StepDeltaData: %w", string(data), err) + } + + u.TextAnnotationDelta = textAnnotationDelta + u.Type = StepDeltaDataTypeTextAnnotationDelta + return nil + case "text": + textDelta := new(TextDelta) + if err := utils.UnmarshalJSON(data, &textDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == text) type TextDelta within StepDeltaData: %w", string(data), err) + } + + u.TextDelta = textDelta + u.Type = StepDeltaDataTypeText + return nil + case "thought_signature": + thoughtSignatureDelta := new(ThoughtSignatureDelta) + if err := utils.UnmarshalJSON(data, &thoughtSignatureDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == thought_signature) type ThoughtSignatureDelta within StepDeltaData: %w", string(data), err) + } + + u.ThoughtSignatureDelta = thoughtSignatureDelta + u.Type = StepDeltaDataTypeThoughtSignature + return nil + case "thought_summary": + thoughtSummaryDelta := new(ThoughtSummaryDelta) + if err := utils.UnmarshalJSON(data, &thoughtSummaryDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == thought_summary) type ThoughtSummaryDelta within StepDeltaData: %w", string(data), err) + } + + u.ThoughtSummaryDelta = thoughtSummaryDelta + u.Type = StepDeltaDataTypeThoughtSummary + return nil + case "url_context_call": + urlContextCallDelta := new(URLContextCallDelta) + if err := utils.UnmarshalJSON(data, &urlContextCallDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_context_call) type URLContextCallDelta within StepDeltaData: %w", string(data), err) + } + + u.URLContextCallDelta = urlContextCallDelta + u.Type = StepDeltaDataTypeURLContextCall + return nil + case "url_context_result": + urlContextResultDelta := new(URLContextResultDelta) + if err := utils.UnmarshalJSON(data, &urlContextResultDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_context_result) type URLContextResultDelta within StepDeltaData: %w", string(data), err) + } + + u.URLContextResultDelta = urlContextResultDelta + u.Type = StepDeltaDataTypeURLContextResult + return nil + case "video": + videoDelta := new(VideoDelta) + if err := utils.UnmarshalJSON(data, &videoDelta, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == video) type VideoDelta within StepDeltaData: %w", string(data), err) + } + + u.VideoDelta = videoDelta + u.Type = StepDeltaDataTypeVideo + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = StepDeltaDataTypeUnknown + return nil + } + +} + +func (u StepDeltaData) MarshalJSON() ([]byte, error) { + if u.ArgumentsDelta != nil { + return utils.MarshalJSON(u.ArgumentsDelta, "", true) + } + + if u.AudioDelta != nil { + return utils.MarshalJSON(u.AudioDelta, "", true) + } + + if u.CodeExecutionCallDelta != nil { + return utils.MarshalJSON(u.CodeExecutionCallDelta, "", true) + } + + if u.CodeExecutionResultDelta != nil { + return utils.MarshalJSON(u.CodeExecutionResultDelta, "", true) + } + + if u.DocumentDelta != nil { + return utils.MarshalJSON(u.DocumentDelta, "", true) + } + + if u.FileSearchCallDelta != nil { + return utils.MarshalJSON(u.FileSearchCallDelta, "", true) + } + + if u.FileSearchResultDelta != nil { + return utils.MarshalJSON(u.FileSearchResultDelta, "", true) + } + + if u.FunctionResultDelta != nil { + return utils.MarshalJSON(u.FunctionResultDelta, "", true) + } + + if u.GoogleMapsCallDelta != nil { + return utils.MarshalJSON(u.GoogleMapsCallDelta, "", true) + } + + if u.GoogleMapsResultDelta != nil { + return utils.MarshalJSON(u.GoogleMapsResultDelta, "", true) + } + + if u.GoogleSearchCallDelta != nil { + return utils.MarshalJSON(u.GoogleSearchCallDelta, "", true) + } + + if u.GoogleSearchResultDelta != nil { + return utils.MarshalJSON(u.GoogleSearchResultDelta, "", true) + } + + if u.ImageDelta != nil { + return utils.MarshalJSON(u.ImageDelta, "", true) + } + + if u.MCPServerToolCallDelta != nil { + return utils.MarshalJSON(u.MCPServerToolCallDelta, "", true) + } + + if u.MCPServerToolResultDelta != nil { + return utils.MarshalJSON(u.MCPServerToolResultDelta, "", true) + } + + if u.ProcessingCallDelta != nil { + return utils.MarshalJSON(u.ProcessingCallDelta, "", true) + } + + if u.ProcessingResultDelta != nil { + return utils.MarshalJSON(u.ProcessingResultDelta, "", true) + } + + if u.RetrievalCallDelta != nil { + return utils.MarshalJSON(u.RetrievalCallDelta, "", true) + } + + if u.RetrievalResultDelta != nil { + return utils.MarshalJSON(u.RetrievalResultDelta, "", true) + } + + if u.TextAnnotationDelta != nil { + return utils.MarshalJSON(u.TextAnnotationDelta, "", true) + } + + if u.TextDelta != nil { + return utils.MarshalJSON(u.TextDelta, "", true) + } + + if u.ThoughtSignatureDelta != nil { + return utils.MarshalJSON(u.ThoughtSignatureDelta, "", true) + } + + if u.ThoughtSummaryDelta != nil { + return utils.MarshalJSON(u.ThoughtSummaryDelta, "", true) + } + + if u.URLContextCallDelta != nil { + return utils.MarshalJSON(u.URLContextCallDelta, "", true) + } + + if u.URLContextResultDelta != nil { + return utils.MarshalJSON(u.URLContextResultDelta, "", true) + } + + if u.VideoDelta != nil { + return utils.MarshalJSON(u.VideoDelta, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type StepDeltaData: all fields are null") +} diff --git a/internal/sdk/models/interactions/stepdeltametadata.go b/internal/sdk/models/interactions/stepdeltametadata.go new file mode 100644 index 0000000..a9f6f4d --- /dev/null +++ b/internal/sdk/models/interactions/stepdeltametadata.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// StepDeltaMetadata - Optional metadata accompanying ANY streamed event. +type StepDeltaMetadata struct { + // Statistics on the interaction request's token usage. + TotalUsage *Usage `json:"total_usage,omitzero"` +} + +func (s StepDeltaMetadata) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StepDeltaMetadata) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StepDeltaMetadata) GetTotalUsage() *Usage { + if s == nil { + return nil + } + return s.TotalUsage +} diff --git a/internal/sdk/models/interactions/stepstart.go b/internal/sdk/models/interactions/stepstart.go new file mode 100644 index 0000000..44647bb --- /dev/null +++ b/internal/sdk/models/interactions/stepstart.go @@ -0,0 +1,152 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StepStart struct { + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"step.start" json:"event_type"` + Index int `json:"index"` + // A step in the interaction. + Step Step `json:"step"` +} + +func (s StepStart) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StepStart) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StepStart) GetEventID() *string { + if s == nil { + return nil + } + return s.EventID +} + +func (s *StepStart) GetEventType() string { + return "step.start" +} + +func (s *StepStart) GetIndex() int { + if s == nil { + return 0 + } + return s.Index +} + +func (s *StepStart) GetStep() Step { + if s == nil { + return Step{} + } + return s.Step +} + +func (s *StepStart) GetStepCodeExecutionCall() *CodeExecutionCallStep { + return s.GetStep().CodeExecutionCallStep +} + +func (s *StepStart) GetStepCodeExecutionResult() *CodeExecutionResultStep { + return s.GetStep().CodeExecutionResultStep +} + +func (s *StepStart) GetStepFileSearchCall() *FileSearchCallStep { + return s.GetStep().FileSearchCallStep +} + +func (s *StepStart) GetStepFileSearchResult() *FileSearchResultStep { + return s.GetStep().FileSearchResultStep +} + +func (s *StepStart) GetStepFunctionCall() *FunctionCallStep { + return s.GetStep().FunctionCallStep +} + +func (s *StepStart) GetStepFunctionResult() *FunctionResultStep { + return s.GetStep().FunctionResultStep +} + +func (s *StepStart) GetStepGoogleMapsCall() *GoogleMapsCallStep { + return s.GetStep().GoogleMapsCallStep +} + +func (s *StepStart) GetStepGoogleMapsResult() *GoogleMapsResultStep { + return s.GetStep().GoogleMapsResultStep +} + +func (s *StepStart) GetStepGoogleSearchCall() *GoogleSearchCallStep { + return s.GetStep().GoogleSearchCallStep +} + +func (s *StepStart) GetStepGoogleSearchResult() *GoogleSearchResultStep { + return s.GetStep().GoogleSearchResultStep +} + +func (s *StepStart) GetStepMcpServerToolCall() *MCPServerToolCallStep { + return s.GetStep().MCPServerToolCallStep +} + +func (s *StepStart) GetStepMcpServerToolResult() *MCPServerToolResultStep { + return s.GetStep().MCPServerToolResultStep +} + +func (s *StepStart) GetStepModelOutput() *ModelOutputStep { + return s.GetStep().ModelOutputStep +} + +func (s *StepStart) GetStepProcessingCall() *ProcessingCallStep { + return s.GetStep().ProcessingCallStep +} + +func (s *StepStart) GetStepProcessingResult() *ProcessingResultStep { + return s.GetStep().ProcessingResultStep +} + +func (s *StepStart) GetStepRetrievalCall() *RetrievalCallStep { + return s.GetStep().RetrievalCallStep +} + +func (s *StepStart) GetStepRetrievalResult() *RetrievalResultStep { + return s.GetStep().RetrievalResultStep +} + +func (s *StepStart) GetStepThought() *ThoughtStep { + return s.GetStep().ThoughtStep +} + +func (s *StepStart) GetStepURLContextCall() *URLContextCallStep { + return s.GetStep().URLContextCallStep +} + +func (s *StepStart) GetStepURLContextResult() *URLContextResultStep { + return s.GetStep().URLContextResultStep +} + +func (s *StepStart) GetStepUserInput() *UserInputStep { + return s.GetStep().UserInputStep +} diff --git a/internal/sdk/models/interactions/stepstop.go b/internal/sdk/models/interactions/stepstop.go new file mode 100644 index 0000000..1d0da18 --- /dev/null +++ b/internal/sdk/models/interactions/stepstop.go @@ -0,0 +1,77 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StepStop struct { + // The event_id token to be used to resume the interaction stream, from + // this event. + EventID *string `json:"event_id,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + eventType string `const:"step.stop" json:"event_type"` + Index int `json:"index"` + // Statistics on the interaction request's token usage. + StepUsage *Usage `json:"step_usage,omitzero"` + // Statistics on the interaction request's token usage. + Usage *Usage `json:"usage,omitzero"` +} + +func (s StepStop) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StepStop) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StepStop) GetEventID() *string { + if s == nil { + return nil + } + return s.EventID +} + +func (s *StepStop) GetEventType() string { + return "step.stop" +} + +func (s *StepStop) GetIndex() int { + if s == nil { + return 0 + } + return s.Index +} + +func (s *StepStop) GetStepUsage() *Usage { + if s == nil { + return nil + } + return s.StepUsage +} + +func (s *StepStop) GetUsage() *Usage { + if s == nil { + return nil + } + return s.Usage +} diff --git a/internal/sdk/models/interactions/textannotationdelta.go b/internal/sdk/models/interactions/textannotationdelta.go new file mode 100644 index 0000000..d1912bf --- /dev/null +++ b/internal/sdk/models/interactions/textannotationdelta.go @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type TextAnnotationDelta struct { + // Citation information for model-generated content. + Annotations []Annotation `json:"annotations,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"text_annotation_delta" json:"type"` +} + +func (t TextAnnotationDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *TextAnnotationDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *TextAnnotationDelta) GetAnnotations() []Annotation { + if t == nil { + return nil + } + return t.Annotations +} + +func (t *TextAnnotationDelta) GetType() string { + return "text_annotation_delta" +} diff --git a/internal/sdk/models/interactions/textcontent.go b/internal/sdk/models/interactions/textcontent.go new file mode 100644 index 0000000..e20b185 --- /dev/null +++ b/internal/sdk/models/interactions/textcontent.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// TextContent - A text content block. +type TextContent struct { + // Citation information for model-generated content. + Annotations []Annotation `json:"annotations,omitzero"` + // Required. The text content. + Text string `json:"text"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"text" json:"type"` +} + +func (t TextContent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *TextContent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *TextContent) GetAnnotations() []Annotation { + if t == nil { + return nil + } + return t.Annotations +} + +func (t *TextContent) GetText() string { + if t == nil { + return "" + } + return t.Text +} + +func (t *TextContent) GetType() string { + return "text" +} diff --git a/internal/sdk/models/interactions/textdelta.go b/internal/sdk/models/interactions/textdelta.go new file mode 100644 index 0000000..c686e7b --- /dev/null +++ b/internal/sdk/models/interactions/textdelta.go @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type TextDelta struct { + Text string `json:"text"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"text" json:"type"` +} + +func (t TextDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *TextDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *TextDelta) GetText() string { + if t == nil { + return "" + } + return t.Text +} + +func (t *TextDelta) GetType() string { + return "text" +} diff --git a/internal/sdk/models/interactions/textresponseformat.go b/internal/sdk/models/interactions/textresponseformat.go new file mode 100644 index 0000000..7451a3c --- /dev/null +++ b/internal/sdk/models/interactions/textresponseformat.go @@ -0,0 +1,84 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// TextResponseFormatMimeType - The MIME type of the text output. +type TextResponseFormatMimeType string + +const ( + TextResponseFormatMimeTypeApplicationJSON TextResponseFormatMimeType = "application/json" + TextResponseFormatMimeTypeTextPlain TextResponseFormatMimeType = "text/plain" +) + +func (e TextResponseFormatMimeType) ToPointer() *TextResponseFormatMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *TextResponseFormatMimeType) IsExact() bool { + if e != nil { + switch *e { + case "application/json", "text/plain": + return true + } + } + return false +} + +// TextResponseFormat - Configuration for text output format. +type TextResponseFormat struct { + // The MIME type of the text output. + MimeType *TextResponseFormatMimeType `json:"mime_type,omitzero"` + // The JSON schema that the output should conform to. Only applicable when + // mime_type is application/json. + Schema map[string]any `json:"schema,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"text" json:"type"` +} + +func (t TextResponseFormat) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *TextResponseFormat) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *TextResponseFormat) GetMimeType() *TextResponseFormatMimeType { + if t == nil { + return nil + } + return t.MimeType +} + +func (t *TextResponseFormat) GetSchema() map[string]any { + if t == nil { + return nil + } + return t.Schema +} + +func (t *TextResponseFormat) GetType() string { + return "text" +} diff --git a/internal/sdk/models/interactions/thinkinglevel.go b/internal/sdk/models/interactions/thinkinglevel.go new file mode 100644 index 0000000..9531c85 --- /dev/null +++ b/internal/sdk/models/interactions/thinkinglevel.go @@ -0,0 +1,41 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type ThinkingLevel string + +const ( + ThinkingLevelMinimal ThinkingLevel = "minimal" + ThinkingLevelLow ThinkingLevel = "low" + ThinkingLevelMedium ThinkingLevel = "medium" + ThinkingLevelHigh ThinkingLevel = "high" +) + +func (e ThinkingLevel) ToPointer() *ThinkingLevel { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ThinkingLevel) IsExact() bool { + if e != nil { + switch *e { + case "minimal", "low", "medium", "high": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/thinkingsummaries.go b/internal/sdk/models/interactions/thinkingsummaries.go new file mode 100644 index 0000000..038abd5 --- /dev/null +++ b/internal/sdk/models/interactions/thinkingsummaries.go @@ -0,0 +1,39 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type ThinkingSummaries string + +const ( + ThinkingSummariesAuto ThinkingSummaries = "auto" + ThinkingSummariesNone ThinkingSummaries = "none" +) + +func (e ThinkingSummaries) ToPointer() *ThinkingSummaries { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ThinkingSummaries) IsExact() bool { + if e != nil { + switch *e { + case "auto", "none": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/thoughtsignaturedelta.go b/internal/sdk/models/interactions/thoughtsignaturedelta.go new file mode 100644 index 0000000..64579b1 --- /dev/null +++ b/internal/sdk/models/interactions/thoughtsignaturedelta.go @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ThoughtSignatureDelta struct { + // Signature to match the backend source to be part of the generation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"thought_signature" json:"type"` +} + +func (t ThoughtSignatureDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *ThoughtSignatureDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *ThoughtSignatureDelta) GetSignature() *string { + if t == nil { + return nil + } + return t.Signature +} + +func (t *ThoughtSignatureDelta) GetType() string { + return "thought_signature" +} diff --git a/internal/sdk/models/interactions/thoughtstep.go b/internal/sdk/models/interactions/thoughtstep.go new file mode 100644 index 0000000..65eb574 --- /dev/null +++ b/internal/sdk/models/interactions/thoughtstep.go @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ThoughtStep - A thought step. +type ThoughtStep struct { + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + // A summary of the thought. + Summary []ThoughtSummaryContent `json:"summary,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"thought" json:"type"` +} + +func (t ThoughtStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *ThoughtStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *ThoughtStep) GetSignature() *string { + if t == nil { + return nil + } + return t.Signature +} + +func (t *ThoughtStep) GetSummary() []ThoughtSummaryContent { + if t == nil { + return nil + } + return t.Summary +} + +func (t *ThoughtStep) GetType() string { + return "thought" +} diff --git a/internal/sdk/models/interactions/thoughtsummarycontent.go b/internal/sdk/models/interactions/thoughtsummarycontent.go new file mode 100644 index 0000000..38a2118 --- /dev/null +++ b/internal/sdk/models/interactions/thoughtsummarycontent.go @@ -0,0 +1,141 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ThoughtSummaryContentType string + +const ( + ThoughtSummaryContentTypeImage ThoughtSummaryContentType = "image" + ThoughtSummaryContentTypeText ThoughtSummaryContentType = "text" + ThoughtSummaryContentTypeUnknown ThoughtSummaryContentType = "UNKNOWN" +) + +type ThoughtSummaryContent struct { + ImageContent *ImageContent `queryParam:"inline" union:"member"` + TextContent *TextContent `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type ThoughtSummaryContentType +} + +func CreateThoughtSummaryContentImage(image ImageContent) ThoughtSummaryContent { + typ := ThoughtSummaryContentTypeImage + + return ThoughtSummaryContent{ + ImageContent: &image, + Type: typ, + } +} + +func CreateThoughtSummaryContentText(text TextContent) ThoughtSummaryContent { + typ := ThoughtSummaryContentTypeText + + return ThoughtSummaryContent{ + TextContent: &text, + Type: typ, + } +} + +func CreateThoughtSummaryContentUnknown(raw json.RawMessage) ThoughtSummaryContent { + return ThoughtSummaryContent{ + UnknownRaw: raw, + Type: ThoughtSummaryContentTypeUnknown, + } +} + +func (u ThoughtSummaryContent) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u ThoughtSummaryContent) IsUnknown() bool { + return u.Type == ThoughtSummaryContentTypeUnknown +} + +func (u *ThoughtSummaryContent) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = ThoughtSummaryContent{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ThoughtSummaryContentTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ThoughtSummaryContentTypeUnknown + return nil + } + + switch dis.Type { + case "image": + imageContent := new(ImageContent) + if err := utils.UnmarshalJSON(data, &imageContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == image) type ImageContent within ThoughtSummaryContent: %w", string(data), err) + } + + u.ImageContent = imageContent + u.Type = ThoughtSummaryContentTypeImage + return nil + case "text": + textContent := new(TextContent) + if err := utils.UnmarshalJSON(data, &textContent, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == text) type TextContent within ThoughtSummaryContent: %w", string(data), err) + } + + u.TextContent = textContent + u.Type = ThoughtSummaryContentTypeText + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = ThoughtSummaryContentTypeUnknown + return nil + } + +} + +func (u ThoughtSummaryContent) MarshalJSON() ([]byte, error) { + if u.ImageContent != nil { + return utils.MarshalJSON(u.ImageContent, "", true) + } + + if u.TextContent != nil { + return utils.MarshalJSON(u.TextContent, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type ThoughtSummaryContent: all fields are null") +} diff --git a/internal/sdk/models/interactions/thoughtsummarydelta.go b/internal/sdk/models/interactions/thoughtsummarydelta.go new file mode 100644 index 0000000..5a60c24 --- /dev/null +++ b/internal/sdk/models/interactions/thoughtsummarydelta.go @@ -0,0 +1,85 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ThoughtSummaryDelta struct { + // The content of the response. + Content *Content `json:"content,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"thought_summary" json:"type"` +} + +func (t ThoughtSummaryDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *ThoughtSummaryDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *ThoughtSummaryDelta) GetContent() *Content { + if t == nil { + return nil + } + return t.Content +} + +func (t *ThoughtSummaryDelta) GetContentAudio() *AudioContent { + if v := t.GetContent(); v != nil { + return v.AudioContent + } + return nil +} + +func (t *ThoughtSummaryDelta) GetContentDocument() *DocumentContent { + if v := t.GetContent(); v != nil { + return v.DocumentContent + } + return nil +} + +func (t *ThoughtSummaryDelta) GetContentImage() *ImageContent { + if v := t.GetContent(); v != nil { + return v.ImageContent + } + return nil +} + +func (t *ThoughtSummaryDelta) GetContentText() *TextContent { + if v := t.GetContent(); v != nil { + return v.TextContent + } + return nil +} + +func (t *ThoughtSummaryDelta) GetContentVideo() *VideoContent { + if v := t.GetContent(); v != nil { + return v.VideoContent + } + return nil +} + +func (t *ThoughtSummaryDelta) GetType() string { + return "thought_summary" +} diff --git a/internal/sdk/models/interactions/tool.go b/internal/sdk/models/interactions/tool.go new file mode 100644 index 0000000..7aa6c3c --- /dev/null +++ b/internal/sdk/models/interactions/tool.go @@ -0,0 +1,310 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ToolType string + +const ( + ToolTypeCodeExecution ToolType = "code_execution" + ToolTypeComputerUse ToolType = "computer_use" + ToolTypeFileSearch ToolType = "file_search" + ToolTypeFunction ToolType = "function" + ToolTypeGoogleMaps ToolType = "google_maps" + ToolTypeGoogleSearch ToolType = "google_search" + ToolTypeMcpServer ToolType = "mcp_server" + ToolTypeRetrieval ToolType = "retrieval" + ToolTypeURLContext ToolType = "url_context" + ToolTypeUnknown ToolType = "UNKNOWN" +) + +// Tool - A tool that can be used by the model. +type Tool struct { + CodeExecution *CodeExecution `queryParam:"inline" union:"member"` + ComputerUse *ComputerUse `queryParam:"inline" union:"member"` + FileSearch *FileSearch `queryParam:"inline" union:"member"` + Function *Function `queryParam:"inline" union:"member"` + GoogleMaps *GoogleMaps `queryParam:"inline" union:"member"` + GoogleSearch *GoogleSearch `queryParam:"inline" union:"member"` + MCPServer *MCPServer `queryParam:"inline" union:"member"` + Retrieval *Retrieval `queryParam:"inline" union:"member"` + URLContext *URLContext `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type ToolType +} + +func CreateToolCodeExecution(codeExecution CodeExecution) Tool { + typ := ToolTypeCodeExecution + + return Tool{ + CodeExecution: &codeExecution, + Type: typ, + } +} + +func CreateToolComputerUse(computerUse ComputerUse) Tool { + typ := ToolTypeComputerUse + + return Tool{ + ComputerUse: &computerUse, + Type: typ, + } +} + +func CreateToolFileSearch(fileSearch FileSearch) Tool { + typ := ToolTypeFileSearch + + return Tool{ + FileSearch: &fileSearch, + Type: typ, + } +} + +func CreateToolFunction(function Function) Tool { + typ := ToolTypeFunction + + return Tool{ + Function: &function, + Type: typ, + } +} + +func CreateToolGoogleMaps(googleMaps GoogleMaps) Tool { + typ := ToolTypeGoogleMaps + + return Tool{ + GoogleMaps: &googleMaps, + Type: typ, + } +} + +func CreateToolGoogleSearch(googleSearch GoogleSearch) Tool { + typ := ToolTypeGoogleSearch + + return Tool{ + GoogleSearch: &googleSearch, + Type: typ, + } +} + +func CreateToolMcpServer(mcpServer MCPServer) Tool { + typ := ToolTypeMcpServer + + return Tool{ + MCPServer: &mcpServer, + Type: typ, + } +} + +func CreateToolRetrieval(retrieval Retrieval) Tool { + typ := ToolTypeRetrieval + + return Tool{ + Retrieval: &retrieval, + Type: typ, + } +} + +func CreateToolURLContext(urlContext URLContext) Tool { + typ := ToolTypeURLContext + + return Tool{ + URLContext: &urlContext, + Type: typ, + } +} + +func CreateToolUnknown(raw json.RawMessage) Tool { + return Tool{ + UnknownRaw: raw, + Type: ToolTypeUnknown, + } +} + +func (u Tool) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Tool) IsUnknown() bool { + return u.Type == ToolTypeUnknown +} + +func (u *Tool) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = Tool{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ToolTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ToolTypeUnknown + return nil + } + + switch dis.Type { + case "code_execution": + codeExecution := new(CodeExecution) + if err := utils.UnmarshalJSON(data, &codeExecution, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == code_execution) type CodeExecution within Tool: %w", string(data), err) + } + + u.CodeExecution = codeExecution + u.Type = ToolTypeCodeExecution + return nil + case "computer_use": + computerUse := new(ComputerUse) + if err := utils.UnmarshalJSON(data, &computerUse, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == computer_use) type ComputerUse within Tool: %w", string(data), err) + } + + u.ComputerUse = computerUse + u.Type = ToolTypeComputerUse + return nil + case "file_search": + fileSearch := new(FileSearch) + if err := utils.UnmarshalJSON(data, &fileSearch, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == file_search) type FileSearch within Tool: %w", string(data), err) + } + + u.FileSearch = fileSearch + u.Type = ToolTypeFileSearch + return nil + case "function": + function := new(Function) + if err := utils.UnmarshalJSON(data, &function, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == function) type Function within Tool: %w", string(data), err) + } + + u.Function = function + u.Type = ToolTypeFunction + return nil + case "google_maps": + googleMaps := new(GoogleMaps) + if err := utils.UnmarshalJSON(data, &googleMaps, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_maps) type GoogleMaps within Tool: %w", string(data), err) + } + + u.GoogleMaps = googleMaps + u.Type = ToolTypeGoogleMaps + return nil + case "google_search": + googleSearch := new(GoogleSearch) + if err := utils.UnmarshalJSON(data, &googleSearch, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == google_search) type GoogleSearch within Tool: %w", string(data), err) + } + + u.GoogleSearch = googleSearch + u.Type = ToolTypeGoogleSearch + return nil + case "mcp_server": + mcpServer := new(MCPServer) + if err := utils.UnmarshalJSON(data, &mcpServer, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == mcp_server) type MCPServer within Tool: %w", string(data), err) + } + + u.MCPServer = mcpServer + u.Type = ToolTypeMcpServer + return nil + case "retrieval": + retrieval := new(Retrieval) + if err := utils.UnmarshalJSON(data, &retrieval, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == retrieval) type Retrieval within Tool: %w", string(data), err) + } + + u.Retrieval = retrieval + u.Type = ToolTypeRetrieval + return nil + case "url_context": + urlContext := new(URLContext) + if err := utils.UnmarshalJSON(data, &urlContext, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == url_context) type URLContext within Tool: %w", string(data), err) + } + + u.URLContext = urlContext + u.Type = ToolTypeURLContext + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = ToolTypeUnknown + return nil + } + +} + +func (u Tool) MarshalJSON() ([]byte, error) { + if u.CodeExecution != nil { + return utils.MarshalJSON(u.CodeExecution, "", true) + } + + if u.ComputerUse != nil { + return utils.MarshalJSON(u.ComputerUse, "", true) + } + + if u.FileSearch != nil { + return utils.MarshalJSON(u.FileSearch, "", true) + } + + if u.Function != nil { + return utils.MarshalJSON(u.Function, "", true) + } + + if u.GoogleMaps != nil { + return utils.MarshalJSON(u.GoogleMaps, "", true) + } + + if u.GoogleSearch != nil { + return utils.MarshalJSON(u.GoogleSearch, "", true) + } + + if u.MCPServer != nil { + return utils.MarshalJSON(u.MCPServer, "", true) + } + + if u.Retrieval != nil { + return utils.MarshalJSON(u.Retrieval, "", true) + } + + if u.URLContext != nil { + return utils.MarshalJSON(u.URLContext, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Tool: all fields are null") +} diff --git a/internal/sdk/models/interactions/toolchoiceconfig.go b/internal/sdk/models/interactions/toolchoiceconfig.go new file mode 100644 index 0000000..0f59262 --- /dev/null +++ b/internal/sdk/models/interactions/toolchoiceconfig.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ToolChoiceConfig - The tool choice configuration containing allowed tools. +type ToolChoiceConfig struct { + // The configuration for allowed tools. + AllowedTools *AllowedTools `json:"allowed_tools,omitzero"` +} + +func (t ToolChoiceConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *ToolChoiceConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *ToolChoiceConfig) GetAllowedTools() *AllowedTools { + if t == nil { + return nil + } + return t.AllowedTools +} diff --git a/internal/sdk/models/interactions/toolchoicetype.go b/internal/sdk/models/interactions/toolchoicetype.go new file mode 100644 index 0000000..e96f158 --- /dev/null +++ b/internal/sdk/models/interactions/toolchoicetype.go @@ -0,0 +1,41 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +type ToolChoiceType string + +const ( + ToolChoiceTypeAuto ToolChoiceType = "auto" + ToolChoiceTypeAny ToolChoiceType = "any" + ToolChoiceTypeNone ToolChoiceType = "none" + ToolChoiceTypeValidated ToolChoiceType = "validated" +) + +func (e ToolChoiceType) ToPointer() *ToolChoiceType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ToolChoiceType) IsExact() bool { + if e != nil { + switch *e { + case "auto", "any", "none", "validated": + return true + } + } + return false +} diff --git a/internal/sdk/models/interactions/transcriptionconfig.go b/internal/sdk/models/interactions/transcriptionconfig.go new file mode 100644 index 0000000..4fe7ed9 --- /dev/null +++ b/internal/sdk/models/interactions/transcriptionconfig.go @@ -0,0 +1,241 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type TranscriptionConfigModeEnum string + +const ( + TranscriptionConfigModeEnumVerbatim TranscriptionConfigModeEnum = "verbatim" + TranscriptionConfigModeEnumSmart TranscriptionConfigModeEnum = "smart" +) + +func (e TranscriptionConfigModeEnum) ToPointer() *TranscriptionConfigModeEnum { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *TranscriptionConfigModeEnum) IsExact() bool { + if e != nil { + switch *e { + case "verbatim", "smart": + return true + } + } + return false +} + +type TranscriptionConfigModeType string + +const ( + TranscriptionConfigModeTypeTranscriptionMode TranscriptionConfigModeType = "TranscriptionMode" + TranscriptionConfigModeTypeTranscriptionConfigModeEnum TranscriptionConfigModeType = "TranscriptionConfigMode_enum" + TranscriptionConfigModeTypeUnknown TranscriptionConfigModeType = "Unknown" +) + +// TranscriptionConfigMode - Discriminated transcription mode options or enum. +type TranscriptionConfigMode struct { + TranscriptionMode *TranscriptionMode `queryParam:"inline" union:"member"` + TranscriptionConfigModeEnum *TranscriptionConfigModeEnum `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type TranscriptionConfigModeType +} + +func CreateTranscriptionConfigModeTranscriptionMode(transcriptionMode TranscriptionMode) TranscriptionConfigMode { + typ := TranscriptionConfigModeTypeTranscriptionMode + + return TranscriptionConfigMode{ + TranscriptionMode: &transcriptionMode, + Type: typ, + } +} + +func CreateTranscriptionConfigModeTranscriptionConfigModeEnum(transcriptionConfigModeEnum TranscriptionConfigModeEnum) TranscriptionConfigMode { + typ := TranscriptionConfigModeTypeTranscriptionConfigModeEnum + + return TranscriptionConfigMode{ + TranscriptionConfigModeEnum: &transcriptionConfigModeEnum, + Type: typ, + } +} + +func CreateTranscriptionConfigModeUnknown(raw json.RawMessage) TranscriptionConfigMode { + return TranscriptionConfigMode{ + UnknownRaw: raw, + Type: TranscriptionConfigModeTypeUnknown, + } +} + +func (u TranscriptionConfigMode) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u TranscriptionConfigMode) IsUnknown() bool { + return u.Type == TranscriptionConfigModeTypeUnknown +} + +func (u *TranscriptionConfigMode) UnmarshalJSON(data []byte) error { + *u = TranscriptionConfigMode{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var transcriptionMode TranscriptionMode = TranscriptionMode{} + if err := utils.UnmarshalJSON(data, &transcriptionMode, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: TranscriptionConfigModeTypeTranscriptionMode, + Value: &transcriptionMode, + }) + } + + var transcriptionConfigModeEnum TranscriptionConfigModeEnum = TranscriptionConfigModeEnum("") + if err := utils.UnmarshalJSON(data, &transcriptionConfigModeEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: TranscriptionConfigModeTypeTranscriptionConfigModeEnum, + Value: &transcriptionConfigModeEnum, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = TranscriptionConfigModeTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = TranscriptionConfigModeTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(TranscriptionConfigModeType) + switch best.Type { + case TranscriptionConfigModeTypeTranscriptionMode: + u.TranscriptionMode = best.Value.(*TranscriptionMode) + return nil + case TranscriptionConfigModeTypeTranscriptionConfigModeEnum: + u.TranscriptionConfigModeEnum = best.Value.(*TranscriptionConfigModeEnum) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = TranscriptionConfigModeTypeUnknown + return nil +} + +func (u TranscriptionConfigMode) MarshalJSON() ([]byte, error) { + if u.TranscriptionMode != nil { + return utils.MarshalJSON(u.TranscriptionMode, "", true) + } + + if u.TranscriptionConfigModeEnum != nil { + return utils.MarshalJSON(u.TranscriptionConfigModeEnum, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type TranscriptionConfigMode: all fields are null") +} + +// TranscriptionConfig - Configuration for speech recognition (transcription). +type TranscriptionConfig struct { + // Optional. A list of phrases to bias the ASR model towards. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + AdaptationPhrases []string `json:"adaptation_phrases,omitzero"` + // Optional. A list of custom vocabulary phrases to bias the speech recognition model + // toward recognizing specific terms. + CustomVocabulary []string `json:"custom_vocabulary,omitzero"` + // Optional. Configures speaker diarization. Supported values: "speaker". + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + DiarizationMode *string `json:"diarization_mode,omitzero"` + // Optional. BCP-47 language codes providing hints about the languages present in the + // audio. If omitted or empty, defaults to automatic language detection. + LanguageCodes []string `json:"language_codes,omitzero"` + // Discriminated transcription mode options or enum. + Mode *TranscriptionConfigMode `json:"mode,omitzero"` + // Optional. The granularity of timestamps to include in the transcription output. + // Supported values: "word". If empty, no timestamps are generated. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + TimestampGranularities []string `json:"timestamp_granularities,omitzero"` +} + +func (t TranscriptionConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *TranscriptionConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *TranscriptionConfig) GetAdaptationPhrases() []string { + if t == nil { + return nil + } + return t.AdaptationPhrases +} + +func (t *TranscriptionConfig) GetCustomVocabulary() []string { + if t == nil { + return nil + } + return t.CustomVocabulary +} + +func (t *TranscriptionConfig) GetDiarizationMode() *string { + if t == nil { + return nil + } + return t.DiarizationMode +} + +func (t *TranscriptionConfig) GetLanguageCodes() []string { + if t == nil { + return nil + } + return t.LanguageCodes +} + +func (t *TranscriptionConfig) GetMode() *TranscriptionConfigMode { + if t == nil { + return nil + } + return t.Mode +} + +func (t *TranscriptionConfig) GetTimestampGranularities() []string { + if t == nil { + return nil + } + return t.TimestampGranularities +} diff --git a/internal/sdk/models/interactions/transcriptionmode.go b/internal/sdk/models/interactions/transcriptionmode.go new file mode 100644 index 0000000..1b826e4 --- /dev/null +++ b/internal/sdk/models/interactions/transcriptionmode.go @@ -0,0 +1,142 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type TranscriptionModeType string + +const ( + TranscriptionModeTypeSmart TranscriptionModeType = "smart" + TranscriptionModeTypeVerbatim TranscriptionModeType = "verbatim" + TranscriptionModeTypeUnknown TranscriptionModeType = "UNKNOWN" +) + +// TranscriptionMode - Configuration for transcription mode. +type TranscriptionMode struct { + SmartTranscriptionMode *SmartTranscriptionMode `queryParam:"inline" union:"member"` + VerbatimTranscriptionMode *VerbatimTranscriptionMode `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type TranscriptionModeType +} + +func CreateTranscriptionModeSmart(smart SmartTranscriptionMode) TranscriptionMode { + typ := TranscriptionModeTypeSmart + + return TranscriptionMode{ + SmartTranscriptionMode: &smart, + Type: typ, + } +} + +func CreateTranscriptionModeVerbatim(verbatim VerbatimTranscriptionMode) TranscriptionMode { + typ := TranscriptionModeTypeVerbatim + + return TranscriptionMode{ + VerbatimTranscriptionMode: &verbatim, + Type: typ, + } +} + +func CreateTranscriptionModeUnknown(raw json.RawMessage) TranscriptionMode { + return TranscriptionMode{ + UnknownRaw: raw, + Type: TranscriptionModeTypeUnknown, + } +} + +func (u TranscriptionMode) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u TranscriptionMode) IsUnknown() bool { + return u.Type == TranscriptionModeTypeUnknown +} + +func (u *TranscriptionMode) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = TranscriptionMode{} + defer func() { + if err != nil { + *u = previous + } + }() + + type discriminator struct { + Type string `json:"type"` + } + + dis := new(discriminator) + if err := json.Unmarshal(data, &dis); err != nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = TranscriptionModeTypeUnknown + return nil + } + if dis == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = TranscriptionModeTypeUnknown + return nil + } + + switch dis.Type { + case "smart": + smartTranscriptionMode := new(SmartTranscriptionMode) + if err := utils.UnmarshalJSON(data, &smartTranscriptionMode, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == smart) type SmartTranscriptionMode within TranscriptionMode: %w", string(data), err) + } + + u.SmartTranscriptionMode = smartTranscriptionMode + u.Type = TranscriptionModeTypeSmart + return nil + case "verbatim": + verbatimTranscriptionMode := new(VerbatimTranscriptionMode) + if err := utils.UnmarshalJSON(data, &verbatimTranscriptionMode, "", true, nil); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Type == verbatim) type VerbatimTranscriptionMode within TranscriptionMode: %w", string(data), err) + } + + u.VerbatimTranscriptionMode = verbatimTranscriptionMode + u.Type = TranscriptionModeTypeVerbatim + return nil + default: + u.UnknownRaw = json.RawMessage(data) + u.Type = TranscriptionModeTypeUnknown + return nil + } + +} + +func (u TranscriptionMode) MarshalJSON() ([]byte, error) { + if u.SmartTranscriptionMode != nil { + return utils.MarshalJSON(u.SmartTranscriptionMode, "", true) + } + + if u.VerbatimTranscriptionMode != nil { + return utils.MarshalJSON(u.VerbatimTranscriptionMode, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type TranscriptionMode: all fields are null") +} diff --git a/internal/sdk/models/interactions/urlcitation.go b/internal/sdk/models/interactions/urlcitation.go new file mode 100644 index 0000000..f4b2802 --- /dev/null +++ b/internal/sdk/models/interactions/urlcitation.go @@ -0,0 +1,80 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// URLCitation - A URL citation annotation. +type URLCitation struct { + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + // The title of the URL. + Title *string `json:"title,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"url_citation" json:"type"` + // The URL. + URL *string `json:"url,omitzero"` +} + +func (u URLCitation) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLCitation) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLCitation) GetEndIndex() *int { + if u == nil { + return nil + } + return u.EndIndex +} + +func (u *URLCitation) GetStartIndex() *int { + if u == nil { + return nil + } + return u.StartIndex +} + +func (u *URLCitation) GetTitle() *string { + if u == nil { + return nil + } + return u.Title +} + +func (u *URLCitation) GetType() string { + return "url_citation" +} + +func (u *URLCitation) GetURL() *string { + if u == nil { + return nil + } + return u.URL +} diff --git a/internal/sdk/models/interactions/urlcontext.go b/internal/sdk/models/interactions/urlcontext.go new file mode 100644 index 0000000..d42f3b1 --- /dev/null +++ b/internal/sdk/models/interactions/urlcontext.go @@ -0,0 +1,42 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// URLContext - A tool that can be used by the model to fetch URL context. +type URLContext struct { + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"url_context" json:"type"` +} + +func (u URLContext) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContext) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContext) GetType() string { + return "url_context" +} diff --git a/internal/sdk/models/interactions/urlcontextcallarguments.go b/internal/sdk/models/interactions/urlcontextcallarguments.go new file mode 100644 index 0000000..402b6bc --- /dev/null +++ b/internal/sdk/models/interactions/urlcontextcallarguments.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// URLContextCallArguments - The arguments to pass to the URL context. +type URLContextCallArguments struct { + // The URLs to fetch. + Urls []string `json:"urls,omitzero"` +} + +func (u URLContextCallArguments) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContextCallArguments) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContextCallArguments) GetUrls() []string { + if u == nil { + return nil + } + return u.Urls +} diff --git a/internal/sdk/models/interactions/urlcontextcalldelta.go b/internal/sdk/models/interactions/urlcontextcalldelta.go new file mode 100644 index 0000000..bb64f7f --- /dev/null +++ b/internal/sdk/models/interactions/urlcontextcalldelta.go @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type URLContextCallDelta struct { + // The arguments to pass to the URL context. + Arguments URLContextCallArguments `json:"arguments"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"url_context_call" json:"type"` +} + +func (u URLContextCallDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContextCallDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContextCallDelta) GetArguments() URLContextCallArguments { + if u == nil { + return URLContextCallArguments{} + } + return u.Arguments +} + +func (u *URLContextCallDelta) GetSignature() *string { + if u == nil { + return nil + } + return u.Signature +} + +func (u *URLContextCallDelta) GetType() string { + return "url_context_call" +} diff --git a/internal/sdk/models/interactions/urlcontextcallstep.go b/internal/sdk/models/interactions/urlcontextcallstep.go new file mode 100644 index 0000000..4620946 --- /dev/null +++ b/internal/sdk/models/interactions/urlcontextcallstep.go @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// URLContextCallStep - URL context call step. +type URLContextCallStep struct { + // The arguments to pass to the URL context. + Arguments URLContextCallArguments `json:"arguments"` + // Required. A unique ID for this specific tool call. + ID string `json:"id"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"url_context_call" json:"type"` +} + +func (u URLContextCallStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContextCallStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContextCallStep) GetArguments() URLContextCallArguments { + if u == nil { + return URLContextCallArguments{} + } + return u.Arguments +} + +func (u *URLContextCallStep) GetID() string { + if u == nil { + return "" + } + return u.ID +} + +func (u *URLContextCallStep) GetSignature() *string { + if u == nil { + return nil + } + return u.Signature +} + +func (u *URLContextCallStep) GetType() string { + return "url_context_call" +} diff --git a/internal/sdk/models/interactions/urlcontextresult.go b/internal/sdk/models/interactions/urlcontextresult.go new file mode 100644 index 0000000..4c09388 --- /dev/null +++ b/internal/sdk/models/interactions/urlcontextresult.go @@ -0,0 +1,79 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// URLContextResultStatus - The status of the URL retrieval. +type URLContextResultStatus string + +const ( + URLContextResultStatusSuccess URLContextResultStatus = "success" + URLContextResultStatusError URLContextResultStatus = "error" + URLContextResultStatusPaywall URLContextResultStatus = "paywall" + URLContextResultStatusUnsafe URLContextResultStatus = "unsafe" +) + +func (e URLContextResultStatus) ToPointer() *URLContextResultStatus { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *URLContextResultStatus) IsExact() bool { + if e != nil { + switch *e { + case "success", "error", "paywall", "unsafe": + return true + } + } + return false +} + +// URLContextResult - The result of the URL context. +type URLContextResult struct { + // The status of the URL retrieval. + Status *URLContextResultStatus `json:"status,omitzero"` + // The URL that was fetched. + URL *string `json:"url,omitzero"` +} + +func (u URLContextResult) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContextResult) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContextResult) GetStatus() *URLContextResultStatus { + if u == nil { + return nil + } + return u.Status +} + +func (u *URLContextResult) GetURL() *string { + if u == nil { + return nil + } + return u.URL +} diff --git a/internal/sdk/models/interactions/urlcontextresultdelta.go b/internal/sdk/models/interactions/urlcontextresultdelta.go new file mode 100644 index 0000000..f0784a5 --- /dev/null +++ b/internal/sdk/models/interactions/urlcontextresultdelta.go @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type URLContextResultDelta struct { + IsError *bool `json:"is_error,omitzero"` + Result []URLContextResult `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"url_context_result" json:"type"` +} + +func (u URLContextResultDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContextResultDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContextResultDelta) GetIsError() *bool { + if u == nil { + return nil + } + return u.IsError +} + +func (u *URLContextResultDelta) GetResult() []URLContextResult { + if u == nil { + return []URLContextResult{} + } + return u.Result +} + +func (u *URLContextResultDelta) GetSignature() *string { + if u == nil { + return nil + } + return u.Signature +} + +func (u *URLContextResultDelta) GetType() string { + return "url_context_result" +} diff --git a/internal/sdk/models/interactions/urlcontextresultstep.go b/internal/sdk/models/interactions/urlcontextresultstep.go new file mode 100644 index 0000000..b6396fd --- /dev/null +++ b/internal/sdk/models/interactions/urlcontextresultstep.go @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// URLContextResultStep - URL context result step. +type URLContextResultStep struct { + // Required. ID to match the ID from the function call block. + CallID string `json:"call_id"` + // Whether the URL context resulted in an error. + IsError *bool `json:"is_error,omitzero"` + // Required. The results of the URL context. + Result []URLContextResult `json:"result"` + // A signature hash for backend validation. + Signature *string `json:"signature,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"url_context_result" json:"type"` +} + +func (u URLContextResultStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *URLContextResultStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *URLContextResultStep) GetCallID() string { + if u == nil { + return "" + } + return u.CallID +} + +func (u *URLContextResultStep) GetIsError() *bool { + if u == nil { + return nil + } + return u.IsError +} + +func (u *URLContextResultStep) GetResult() []URLContextResult { + if u == nil { + return []URLContextResult{} + } + return u.Result +} + +func (u *URLContextResultStep) GetSignature() *string { + if u == nil { + return nil + } + return u.Signature +} + +func (u *URLContextResultStep) GetType() string { + return "url_context_result" +} diff --git a/internal/sdk/models/interactions/usage.go b/internal/sdk/models/interactions/usage.go new file mode 100644 index 0000000..f0352c1 --- /dev/null +++ b/internal/sdk/models/interactions/usage.go @@ -0,0 +1,136 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Usage - Statistics on the interaction request's token usage. +type Usage struct { + // A breakdown of cached token usage by modality. + CachedTokensByModality []ModalityTokens `json:"cached_tokens_by_modality,omitzero"` + // Grounding tool count. + GroundingToolCount []GroundingToolCount `json:"grounding_tool_count,omitzero"` + // A breakdown of input token usage by modality. + InputTokensByModality []ModalityTokens `json:"input_tokens_by_modality,omitzero"` + // A breakdown of output token usage by modality. + OutputTokensByModality []ModalityTokens `json:"output_tokens_by_modality,omitzero"` + // A breakdown of tool-use token usage by modality. + ToolUseTokensByModality []ModalityTokens `json:"tool_use_tokens_by_modality,omitzero"` + // Number of tokens in the cached part of the prompt (the cached content). + TotalCachedTokens *int `json:"total_cached_tokens,omitzero"` + // Number of tokens in the prompt (context). + TotalInputTokens *int `json:"total_input_tokens,omitzero"` + // Total number of tokens across all the generated responses. + TotalOutputTokens *int `json:"total_output_tokens,omitzero"` + // Number of tokens of thoughts for thinking models. + TotalThoughtTokens *int `json:"total_thought_tokens,omitzero"` + // Total token count for the interaction request (prompt + responses + other + // internal tokens). + TotalTokens *int `json:"total_tokens,omitzero"` + // Number of tokens present in tool-use prompt(s). + TotalToolUseTokens *int `json:"total_tool_use_tokens,omitzero"` +} + +func (u Usage) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *Usage) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *Usage) GetCachedTokensByModality() []ModalityTokens { + if u == nil { + return nil + } + return u.CachedTokensByModality +} + +func (u *Usage) GetGroundingToolCount() []GroundingToolCount { + if u == nil { + return nil + } + return u.GroundingToolCount +} + +func (u *Usage) GetInputTokensByModality() []ModalityTokens { + if u == nil { + return nil + } + return u.InputTokensByModality +} + +func (u *Usage) GetOutputTokensByModality() []ModalityTokens { + if u == nil { + return nil + } + return u.OutputTokensByModality +} + +func (u *Usage) GetToolUseTokensByModality() []ModalityTokens { + if u == nil { + return nil + } + return u.ToolUseTokensByModality +} + +func (u *Usage) GetTotalCachedTokens() *int { + if u == nil { + return nil + } + return u.TotalCachedTokens +} + +func (u *Usage) GetTotalInputTokens() *int { + if u == nil { + return nil + } + return u.TotalInputTokens +} + +func (u *Usage) GetTotalOutputTokens() *int { + if u == nil { + return nil + } + return u.TotalOutputTokens +} + +func (u *Usage) GetTotalThoughtTokens() *int { + if u == nil { + return nil + } + return u.TotalThoughtTokens +} + +func (u *Usage) GetTotalTokens() *int { + if u == nil { + return nil + } + return u.TotalTokens +} + +func (u *Usage) GetTotalToolUseTokens() *int { + if u == nil { + return nil + } + return u.TotalToolUseTokens +} diff --git a/internal/sdk/models/interactions/userinputstep.go b/internal/sdk/models/interactions/userinputstep.go new file mode 100644 index 0000000..90df36c --- /dev/null +++ b/internal/sdk/models/interactions/userinputstep.go @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// UserInputStep - Input provided by the user. +type UserInputStep struct { + Content []Content `json:"content,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"user_input" json:"type"` +} + +func (u UserInputStep) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UserInputStep) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UserInputStep) GetContent() []Content { + if u == nil { + return nil + } + return u.Content +} + +func (u *UserInputStep) GetType() string { + return "user_input" +} diff --git a/internal/sdk/models/interactions/verbatimtranscriptionmode.go b/internal/sdk/models/interactions/verbatimtranscriptionmode.go new file mode 100644 index 0000000..6851911 --- /dev/null +++ b/internal/sdk/models/interactions/verbatimtranscriptionmode.go @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// VerbatimTranscriptionMode - Configuration for verbatim transcription mode. +type VerbatimTranscriptionMode struct { + // Optional. Configures speaker diarization. Supported values: "speaker". + DiarizationMode *string `json:"diarization_mode,omitzero"` + // Optional. The granularity of timestamps to include in the transcription output. + // Supported values: "word". If empty, no timestamps are generated. + TimestampGranularities []string `json:"timestamp_granularities,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"verbatim" json:"type"` +} + +func (v VerbatimTranscriptionMode) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *VerbatimTranscriptionMode) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, nil); err != nil { + return err + } + return nil +} + +func (v *VerbatimTranscriptionMode) GetDiarizationMode() *string { + if v == nil { + return nil + } + return v.DiarizationMode +} + +func (v *VerbatimTranscriptionMode) GetTimestampGranularities() []string { + if v == nil { + return nil + } + return v.TimestampGranularities +} + +func (v *VerbatimTranscriptionMode) GetType() string { + return "verbatim" +} diff --git a/internal/sdk/models/interactions/vertexaisearchconfig.go b/internal/sdk/models/interactions/vertexaisearchconfig.go new file mode 100644 index 0000000..dc59b59 --- /dev/null +++ b/internal/sdk/models/interactions/vertexaisearchconfig.go @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// VertexAISearchConfig - Used to specify configuration for VertexAISearch. +type VertexAISearchConfig struct { + // Optional. Used to specify Vertex AI Search datastores. + Datastores []string `json:"datastores,omitzero"` + // Optional. Used to specify Vertex AI Search engine. + Engine *string `json:"engine,omitzero"` +} + +func (v VertexAISearchConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *VertexAISearchConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, nil); err != nil { + return err + } + return nil +} + +func (v *VertexAISearchConfig) GetDatastores() []string { + if v == nil { + return nil + } + return v.Datastores +} + +func (v *VertexAISearchConfig) GetEngine() *string { + if v == nil { + return nil + } + return v.Engine +} diff --git a/internal/sdk/models/interactions/videoconfig.go b/internal/sdk/models/interactions/videoconfig.go new file mode 100644 index 0000000..c20f21e --- /dev/null +++ b/internal/sdk/models/interactions/videoconfig.go @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// Task - Optional task mode for video generation. If not specified, the model +// automatically determines the appropriate mode based on the provided text +// prompt and input media. +type Task string + +const ( + TaskTextToVideo Task = "text_to_video" + TaskImageToVideo Task = "image_to_video" + TaskReferenceToVideo Task = "reference_to_video" + TaskEdit Task = "edit" + TaskExtend Task = "extend" +) + +func (e Task) ToPointer() *Task { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Task) IsExact() bool { + if e != nil { + switch *e { + case "text_to_video", "image_to_video", "reference_to_video", "edit", "extend": + return true + } + } + return false +} + +// VideoConfig - Configuration options for video generation. +type VideoConfig struct { + // Optional task mode for video generation. If not specified, the model + // automatically determines the appropriate mode based on the provided text + // prompt and input media. + Task *Task `json:"task,omitzero"` +} + +func (v VideoConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *VideoConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, nil); err != nil { + return err + } + return nil +} + +func (v *VideoConfig) GetTask() *Task { + if v == nil { + return nil + } + return v.Task +} diff --git a/internal/sdk/models/interactions/videocontent.go b/internal/sdk/models/interactions/videocontent.go new file mode 100644 index 0000000..2a50420 --- /dev/null +++ b/internal/sdk/models/interactions/videocontent.go @@ -0,0 +1,268 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "encoding/json" + "errors" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// VideoContentMimeType - The mime type of the video. +type VideoContentMimeType string + +const ( + VideoContentMimeTypeVideoMp4 VideoContentMimeType = "video/mp4" + VideoContentMimeTypeVideoMpeg VideoContentMimeType = "video/mpeg" + VideoContentMimeTypeVideoMpg VideoContentMimeType = "video/mpg" + VideoContentMimeTypeVideoMov VideoContentMimeType = "video/mov" + VideoContentMimeTypeVideoAvi VideoContentMimeType = "video/avi" + VideoContentMimeTypeVideoXFlv VideoContentMimeType = "video/x-flv" + VideoContentMimeTypeVideoWebm VideoContentMimeType = "video/webm" + VideoContentMimeTypeVideoWmv VideoContentMimeType = "video/wmv" + VideoContentMimeTypeVideo3gpp VideoContentMimeType = "video/3gpp" +) + +func (e VideoContentMimeType) ToPointer() *VideoContentMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *VideoContentMimeType) IsExact() bool { + if e != nil { + switch *e { + case "video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", "video/x-flv", "video/webm", "video/wmv", "video/3gpp": + return true + } + } + return false +} + +type ProcessingEnum string + +const ( + ProcessingEnumStatic ProcessingEnum = "static" + ProcessingEnumAgentic ProcessingEnum = "agentic" +) + +func (e ProcessingEnum) ToPointer() *ProcessingEnum { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *ProcessingEnum) IsExact() bool { + if e != nil { + switch *e { + case "static", "agentic": + return true + } + } + return false +} + +type ProcessingType string + +const ( + ProcessingTypeMediaProcessing ProcessingType = "MediaProcessing" + ProcessingTypeProcessingEnum ProcessingType = "processing_enum" + ProcessingTypeUnknown ProcessingType = "Unknown" +) + +// Processing - How the model processes this video for understanding. +type Processing struct { + MediaProcessing *MediaProcessing `queryParam:"inline" union:"member"` + ProcessingEnum *ProcessingEnum `queryParam:"inline" union:"member"` + UnknownRaw json.RawMessage `json:"-" union:"unknown"` + + Type ProcessingType +} + +func CreateProcessingMediaProcessing(mediaProcessing MediaProcessing) Processing { + typ := ProcessingTypeMediaProcessing + + return Processing{ + MediaProcessing: &mediaProcessing, + Type: typ, + } +} + +func CreateProcessingProcessingEnum(processingEnum ProcessingEnum) Processing { + typ := ProcessingTypeProcessingEnum + + return Processing{ + ProcessingEnum: &processingEnum, + Type: typ, + } +} + +func CreateProcessingUnknown(raw json.RawMessage) Processing { + return Processing{ + UnknownRaw: raw, + Type: ProcessingTypeUnknown, + } +} + +func (u Processing) GetUnknownRaw() json.RawMessage { + return u.UnknownRaw +} + +func (u Processing) IsUnknown() bool { + return u.Type == ProcessingTypeUnknown +} + +func (u *Processing) UnmarshalJSON(data []byte) error { + *u = Processing{} + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var mediaProcessing MediaProcessing = MediaProcessing{} + if err := utils.UnmarshalJSON(data, &mediaProcessing, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ProcessingTypeMediaProcessing, + Value: &mediaProcessing, + }) + } + + var processingEnum ProcessingEnum = ProcessingEnum("") + if err := utils.UnmarshalJSON(data, &processingEnum, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: ProcessingTypeProcessingEnum, + Value: &processingEnum, + }) + } + + if len(candidates) == 0 { + u.UnknownRaw = json.RawMessage(data) + u.Type = ProcessingTypeUnknown + return nil + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + u.UnknownRaw = json.RawMessage(data) + u.Type = ProcessingTypeUnknown + return nil + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(ProcessingType) + switch best.Type { + case ProcessingTypeMediaProcessing: + u.MediaProcessing = best.Value.(*MediaProcessing) + return nil + case ProcessingTypeProcessingEnum: + u.ProcessingEnum = best.Value.(*ProcessingEnum) + return nil + } + + u.UnknownRaw = json.RawMessage(data) + u.Type = ProcessingTypeUnknown + return nil +} + +func (u Processing) MarshalJSON() ([]byte, error) { + if u.MediaProcessing != nil { + return utils.MarshalJSON(u.MediaProcessing, "", true) + } + + if u.ProcessingEnum != nil { + return utils.MarshalJSON(u.ProcessingEnum, "", true) + } + + if u.UnknownRaw != nil { + return json.RawMessage(u.UnknownRaw), nil + } + return nil, errors.New("could not marshal union type Processing: all fields are null") +} + +// VideoContent - A video content block. +type VideoContent struct { + // The video content. + Data *string `json:"data,omitzero"` + // The mime type of the video. + MimeType *VideoContentMimeType `json:"mime_type,omitzero"` + // A user-defined name for this content block. Can be referenced by the model + // in the final response. + Name *string `json:"name,omitzero"` + // How the model processes this video for understanding. + Processing *Processing `json:"processing,omitzero"` + Resolution *MediaResolution `json:"resolution,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"video" json:"type"` + // The URI of the video. + URI *string `json:"uri,omitzero"` +} + +func (v VideoContent) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *VideoContent) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, nil); err != nil { + return err + } + return nil +} + +func (v *VideoContent) GetData() *string { + if v == nil { + return nil + } + return v.Data +} + +func (v *VideoContent) GetMimeType() *VideoContentMimeType { + if v == nil { + return nil + } + return v.MimeType +} + +func (v *VideoContent) GetName() *string { + if v == nil { + return nil + } + return v.Name +} + +func (v *VideoContent) GetProcessing() *Processing { + if v == nil { + return nil + } + return v.Processing +} + +func (v *VideoContent) GetResolution() *MediaResolution { + if v == nil { + return nil + } + return v.Resolution +} + +func (v *VideoContent) GetType() string { + return "video" +} + +func (v *VideoContent) GetURI() *string { + if v == nil { + return nil + } + return v.URI +} diff --git a/internal/sdk/models/interactions/videodelta.go b/internal/sdk/models/interactions/videodelta.go new file mode 100644 index 0000000..056d88d --- /dev/null +++ b/internal/sdk/models/interactions/videodelta.go @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type VideoDeltaMimeType string + +const ( + VideoDeltaMimeTypeVideoMp4 VideoDeltaMimeType = "video/mp4" + VideoDeltaMimeTypeVideoMpeg VideoDeltaMimeType = "video/mpeg" + VideoDeltaMimeTypeVideoMpg VideoDeltaMimeType = "video/mpg" + VideoDeltaMimeTypeVideoMov VideoDeltaMimeType = "video/mov" + VideoDeltaMimeTypeVideoAvi VideoDeltaMimeType = "video/avi" + VideoDeltaMimeTypeVideoXFlv VideoDeltaMimeType = "video/x-flv" + VideoDeltaMimeTypeVideoWebm VideoDeltaMimeType = "video/webm" + VideoDeltaMimeTypeVideoWmv VideoDeltaMimeType = "video/wmv" + VideoDeltaMimeTypeVideo3gpp VideoDeltaMimeType = "video/3gpp" + VideoDeltaMimeTypeVideoJpeg2000 VideoDeltaMimeType = "video/jpeg2000" +) + +func (e VideoDeltaMimeType) ToPointer() *VideoDeltaMimeType { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *VideoDeltaMimeType) IsExact() bool { + if e != nil { + switch *e { + case "video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", "video/x-flv", "video/webm", "video/wmv", "video/3gpp", "video/jpeg2000": + return true + } + } + return false +} + +type VideoDelta struct { + Data *string `json:"data,omitzero"` + MimeType *VideoDeltaMimeType `json:"mime_type,omitzero"` + Resolution *MediaResolution `json:"resolution,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"video" json:"type"` + URI *string `json:"uri,omitzero"` +} + +func (v VideoDelta) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *VideoDelta) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, nil); err != nil { + return err + } + return nil +} + +func (v *VideoDelta) GetData() *string { + if v == nil { + return nil + } + return v.Data +} + +func (v *VideoDelta) GetMimeType() *VideoDeltaMimeType { + if v == nil { + return nil + } + return v.MimeType +} + +func (v *VideoDelta) GetResolution() *MediaResolution { + if v == nil { + return nil + } + return v.Resolution +} + +func (v *VideoDelta) GetType() string { + return "video" +} + +func (v *VideoDelta) GetURI() *string { + if v == nil { + return nil + } + return v.URI +} diff --git a/internal/sdk/models/interactions/videoresponseformat.go b/internal/sdk/models/interactions/videoresponseformat.go new file mode 100644 index 0000000..3f4fbd9 --- /dev/null +++ b/internal/sdk/models/interactions/videoresponseformat.go @@ -0,0 +1,159 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// VideoResponseFormatAspectRatio - The aspect ratio for the video output. +type VideoResponseFormatAspectRatio string + +const ( + VideoResponseFormatAspectRatioOneHundredAndSixtyNine VideoResponseFormatAspectRatio = "16:9" + VideoResponseFormatAspectRatioNineHundredAndSixteen VideoResponseFormatAspectRatio = "9:16" +) + +func (e VideoResponseFormatAspectRatio) ToPointer() *VideoResponseFormatAspectRatio { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *VideoResponseFormatAspectRatio) IsExact() bool { + if e != nil { + switch *e { + case "16:9", "9:16": + return true + } + } + return false +} + +// VideoResponseFormatDelivery - The delivery mode for the video output. +type VideoResponseFormatDelivery string + +const ( + VideoResponseFormatDeliveryInline VideoResponseFormatDelivery = "inline" + VideoResponseFormatDeliveryURI VideoResponseFormatDelivery = "uri" +) + +func (e VideoResponseFormatDelivery) ToPointer() *VideoResponseFormatDelivery { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *VideoResponseFormatDelivery) IsExact() bool { + if e != nil { + switch *e { + case "inline", "uri": + return true + } + } + return false +} + +// Resolution - The video output resolution. Defaults to 720p. +type Resolution string + +const ( + ResolutionThreeHundredAndSixtyp Resolution = "360p" + ResolutionSevenHundredAndTwentyp Resolution = "720p" + ResolutionOneThousandAndEightyp Resolution = "1080p" + ResolutionFourk Resolution = "4k" +) + +func (e Resolution) ToPointer() *Resolution { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *Resolution) IsExact() bool { + if e != nil { + switch *e { + case "360p", "720p", "1080p", "4k": + return true + } + } + return false +} + +// VideoResponseFormat - Configuration for video output format. +type VideoResponseFormat struct { + // The aspect ratio for the video output. + AspectRatio *VideoResponseFormatAspectRatio `json:"aspect_ratio,omitzero"` + // The delivery mode for the video output. + Delivery *VideoResponseFormatDelivery `json:"delivery,omitzero"` + // The duration for the video output. + Duration *string `json:"duration,omitzero"` + // The Cloud Storage URI to store the video output. Required for Vertex if + // delivery mode is URI. + GcsURI *string `json:"gcs_uri,omitzero"` + // The video output resolution. Defaults to 720p. + Resolution *Resolution `json:"resolution,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"video" json:"type"` +} + +func (v VideoResponseFormat) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *VideoResponseFormat) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, nil); err != nil { + return err + } + return nil +} + +func (v *VideoResponseFormat) GetAspectRatio() *VideoResponseFormatAspectRatio { + if v == nil { + return nil + } + return v.AspectRatio +} + +func (v *VideoResponseFormat) GetDelivery() *VideoResponseFormatDelivery { + if v == nil { + return nil + } + return v.Delivery +} + +func (v *VideoResponseFormat) GetDuration() *string { + if v == nil { + return nil + } + return v.Duration +} + +func (v *VideoResponseFormat) GetGcsURI() *string { + if v == nil { + return nil + } + return v.GcsURI +} + +func (v *VideoResponseFormat) GetResolution() *Resolution { + if v == nil { + return nil + } + return v.Resolution +} + +func (v *VideoResponseFormat) GetType() string { + return "video" +} diff --git a/internal/sdk/models/interactions/webhookconfig.go b/internal/sdk/models/interactions/webhookconfig.go new file mode 100644 index 0000000..d21ddf2 --- /dev/null +++ b/internal/sdk/models/interactions/webhookconfig.go @@ -0,0 +1,56 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// WebhookConfig - Message for configuring webhook events for a request. +type WebhookConfig struct { + // Optional. If set, these webhook URIs will be used for webhook events instead of the + // registered webhooks. + Uris []string `json:"uris,omitzero"` + // Optional. The user metadata that will be returned on each event emission to the + // webhooks. + UserMetadata map[string]any `json:"user_metadata,omitzero"` +} + +func (w WebhookConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(w, "", false) +} + +func (w *WebhookConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { + return err + } + return nil +} + +func (w *WebhookConfig) GetUris() []string { + if w == nil { + return nil + } + return w.Uris +} + +func (w *WebhookConfig) GetUserMetadata() map[string]any { + if w == nil { + return nil + } + return w.UserMetadata +} diff --git a/internal/sdk/models/interactions/wordinfo.go b/internal/sdk/models/interactions/wordinfo.go new file mode 100644 index 0000000..ba6e782 --- /dev/null +++ b/internal/sdk/models/interactions/wordinfo.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package interactions + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// WordInfo - Word-level ASR annotation for transcription output. +// Carries the word text, optional timing, and optional speaker attribution. +type WordInfo struct { + // End of the attributed segment, exclusive. + EndIndex *int `json:"end_index,omitzero"` + // End offset in time of the word relative to the start of the audio. + // Present when timestamp_granularities contains "word". + EndOffset *string `json:"end_offset,omitzero"` + // Optional. Speaker label for this word (e.g. "spk_1", "spk_2"). + // Present when diarization_mode is set in TranscriptionConfig. + Speaker *string `json:"speaker,omitzero"` + // Start of segment of the response that is attributed to this source. + // + // Index indicates the start of the segment, measured in bytes. + StartIndex *int `json:"start_index,omitzero"` + // Start offset in time of the word relative to the start of the audio. + // Present when timestamp_granularities contains "word". + StartOffset *string `json:"start_offset,omitzero"` + // The transcribed word. + Text *string `json:"text,omitzero"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + type_ string `const:"word_info" json:"type"` +} + +func (w WordInfo) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(w, "", false) +} + +func (w *WordInfo) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { + return err + } + return nil +} + +func (w *WordInfo) GetEndIndex() *int { + if w == nil { + return nil + } + return w.EndIndex +} + +func (w *WordInfo) GetEndOffset() *string { + if w == nil { + return nil + } + return w.EndOffset +} + +func (w *WordInfo) GetSpeaker() *string { + if w == nil { + return nil + } + return w.Speaker +} + +func (w *WordInfo) GetStartIndex() *int { + if w == nil { + return nil + } + return w.StartIndex +} + +func (w *WordInfo) GetStartOffset() *string { + if w == nil { + return nil + } + return w.StartOffset +} + +func (w *WordInfo) GetText() *string { + if w == nil { + return nil + } + return w.Text +} + +func (w *WordInfo) GetType() string { + return "word_info" +} diff --git a/internal/sdk/models/operations/cancelinteractionbyid.go b/internal/sdk/models/operations/cancelinteractionbyid.go new file mode 100644 index 0000000..28c8b38 --- /dev/null +++ b/internal/sdk/models/operations/cancelinteractionbyid.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CancelInteractionByIDGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (c CancelInteractionByIDGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CancelInteractionByIDGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CancelInteractionByIDGlobals) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CancelInteractionByIDGlobals) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +type CancelInteractionByIDRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The unique identifier of the interaction to cancel. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (c CancelInteractionByIDRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CancelInteractionByIDRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CancelInteractionByIDRequest) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +func (c *CancelInteractionByIDRequest) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CancelInteractionByIDRequest) GetID() string { + if c == nil { + return "" + } + return c.ID +} + +type CancelInteractionByIDResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful cancellation of the interaction. + Interaction *interactions.Interaction +} + +func (c CancelInteractionByIDResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CancelInteractionByIDResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CancelInteractionByIDResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CancelInteractionByIDResponse) GetInteraction() *interactions.Interaction { + if c == nil { + return nil + } + return c.Interaction +} diff --git a/internal/sdk/models/operations/createagent.go b/internal/sdk/models/operations/createagent.go new file mode 100644 index 0000000..c3f047c --- /dev/null +++ b/internal/sdk/models/operations/createagent.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/agents" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateAgentGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (c CreateAgentGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateAgentGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateAgentGlobals) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateAgentGlobals) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +type CreateAgentRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The request body. + Body agents.Agent `request:"mediaType=application/json"` +} + +func (c CreateAgentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateAgentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateAgentRequest) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +func (c *CreateAgentRequest) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateAgentRequest) GetBody() agents.Agent { + if c == nil { + return agents.Agent{} + } + return c.Body +} + +type CreateAgentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Agent *agents.Agent +} + +func (c CreateAgentResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateAgentResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateAgentResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CreateAgentResponse) GetAgent() *agents.Agent { + if c == nil { + return nil + } + return c.Agent +} diff --git a/internal/sdk/models/operations/createcredential.go b/internal/sdk/models/operations/createcredential.go new file mode 100644 index 0000000..d364b07 --- /dev/null +++ b/internal/sdk/models/operations/createcredential.go @@ -0,0 +1,120 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/credentials" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateCredentialGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (c CreateCredentialGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateCredentialGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateCredentialGlobals) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +type CreateCredentialRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + Body credentials.CredentialCreateParams `request:"mediaType=application/json"` +} + +func (c CreateCredentialRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateCredentialRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateCredentialRequest) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateCredentialRequest) GetBody() credentials.CredentialCreateParams { + if c == nil { + return credentials.CredentialCreateParams{} + } + return c.Body +} + +func (c *CreateCredentialRequest) GetBodyEnvironmentVariable() *credentials.EnvironmentVariableConfig { + return c.GetBody().EnvironmentVariableConfig +} + +func (c *CreateCredentialRequest) GetBodyBearerToken() *credentials.HTTPBearerConfig { + return c.GetBody().HTTPBearerConfig +} + +func (c *CreateCredentialRequest) GetBodyOauth2() *credentials.OAuth2Config { + return c.GetBody().OAuth2Config +} + +type CreateCredentialResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Credential *credentials.Credential +} + +func (c CreateCredentialResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateCredentialResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateCredentialResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CreateCredentialResponse) GetCredential() *credentials.Credential { + if c == nil { + return nil + } + return c.Credential +} diff --git a/internal/sdk/models/operations/createenvironment.go b/internal/sdk/models/operations/createenvironment.go new file mode 100644 index 0000000..b70932d --- /dev/null +++ b/internal/sdk/models/operations/createenvironment.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/environments" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateEnvironmentGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (c CreateEnvironmentGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateEnvironmentGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateEnvironmentGlobals) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +type CreateEnvironmentRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The environment to create. + Body environments.CreateEnvironmentRequest `request:"mediaType=application/json"` +} + +func (c CreateEnvironmentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateEnvironmentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateEnvironmentRequest) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateEnvironmentRequest) GetBody() environments.CreateEnvironmentRequest { + if c == nil { + return environments.CreateEnvironmentRequest{} + } + return c.Body +} + +type CreateEnvironmentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Environment *environments.Environment +} + +func (c CreateEnvironmentResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateEnvironmentResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateEnvironmentResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CreateEnvironmentResponse) GetEnvironment() *environments.Environment { + if c == nil { + return nil + } + return c.Environment +} diff --git a/internal/sdk/models/operations/createinteraction.go b/internal/sdk/models/operations/createinteraction.go new file mode 100644 index 0000000..1b2b7f3 --- /dev/null +++ b/internal/sdk/models/operations/createinteraction.go @@ -0,0 +1,237 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "errors" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types/stream" +) + +type CreateInteractionGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (c CreateInteractionGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateInteractionGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateInteractionGlobals) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateInteractionGlobals) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +type CreateInteractionRequestBodyType string + +const ( + CreateInteractionRequestBodyTypeCreateAgentInteraction CreateInteractionRequestBodyType = "CreateAgentInteraction" + CreateInteractionRequestBodyTypeCreateModelInteraction CreateInteractionRequestBodyType = "CreateModelInteraction" +) + +// CreateInteractionRequestBody - The request body. +type CreateInteractionRequestBody struct { + CreateAgentInteraction *interactions.CreateAgentInteraction `queryParam:"inline" union:"member"` + CreateModelInteraction *interactions.CreateModelInteraction `queryParam:"inline" union:"member"` + + Type CreateInteractionRequestBodyType +} + +func CreateCreateInteractionRequestBodyCreateAgentInteraction(createAgentInteraction interactions.CreateAgentInteraction) CreateInteractionRequestBody { + typ := CreateInteractionRequestBodyTypeCreateAgentInteraction + + return CreateInteractionRequestBody{ + CreateAgentInteraction: &createAgentInteraction, + Type: typ, + } +} + +func CreateCreateInteractionRequestBodyCreateModelInteraction(createModelInteraction interactions.CreateModelInteraction) CreateInteractionRequestBody { + typ := CreateInteractionRequestBodyTypeCreateModelInteraction + + return CreateInteractionRequestBody{ + CreateModelInteraction: &createModelInteraction, + Type: typ, + } +} + +func (u *CreateInteractionRequestBody) UnmarshalJSON(data []byte) (err error) { + previous := *u + *u = CreateInteractionRequestBody{} + defer func() { + if err != nil { + *u = previous + } + }() + + var candidates []utils.UnionCandidate + + // Collect all valid candidates + var createAgentInteraction interactions.CreateAgentInteraction = interactions.CreateAgentInteraction{} + if err := utils.UnmarshalJSON(data, &createAgentInteraction, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateInteractionRequestBodyTypeCreateAgentInteraction, + Value: &createAgentInteraction, + }) + } + + var createModelInteraction interactions.CreateModelInteraction = interactions.CreateModelInteraction{} + if err := utils.UnmarshalJSON(data, &createModelInteraction, "", true, nil); err == nil { + candidates = append(candidates, utils.UnionCandidate{ + Type: CreateInteractionRequestBodyTypeCreateModelInteraction, + Value: &createModelInteraction, + }) + } + + if len(candidates) == 0 { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateInteractionRequestBody", string(data)) + } + + // Pick the best candidate using multi-stage filtering + best := utils.PickBestUnionCandidate(candidates, data) + if best == nil { + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateInteractionRequestBody", string(data)) + } + + // Set the union type and value based on the best candidate + u.Type = best.Type.(CreateInteractionRequestBodyType) + switch best.Type { + case CreateInteractionRequestBodyTypeCreateAgentInteraction: + u.CreateAgentInteraction = best.Value.(*interactions.CreateAgentInteraction) + return nil + case CreateInteractionRequestBodyTypeCreateModelInteraction: + u.CreateModelInteraction = best.Value.(*interactions.CreateModelInteraction) + return nil + } + + return fmt.Errorf("could not unmarshal `%s` into any supported union types for CreateInteractionRequestBody", string(data)) +} + +func (u CreateInteractionRequestBody) MarshalJSON() ([]byte, error) { + if u.CreateAgentInteraction != nil { + return utils.MarshalJSON(u.CreateAgentInteraction, "", true) + } + + if u.CreateModelInteraction != nil { + return utils.MarshalJSON(u.CreateModelInteraction, "", true) + } + + return nil, errors.New("could not marshal union type CreateInteractionRequestBody: all fields are null") +} + +type CreateInteractionRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The request body. + Body CreateInteractionRequestBody `request:"mediaType=application/json"` +} + +func (c CreateInteractionRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateInteractionRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateInteractionRequest) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +func (c *CreateInteractionRequest) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateInteractionRequest) GetBody() CreateInteractionRequestBody { + if c == nil { + return CreateInteractionRequestBody{} + } + return c.Body +} + +type CreateInteractionResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Interaction *interactions.Interaction + // Successful operation + InteractionSSEStreamEvent *stream.EventStream[interactions.InteractionSSEStreamEvent] +} + +func (c CreateInteractionResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateInteractionResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateInteractionResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CreateInteractionResponse) GetInteraction() *interactions.Interaction { + if c == nil { + return nil + } + return c.Interaction +} + +func (c *CreateInteractionResponse) GetInteractionSSEStreamEvent() *stream.EventStream[interactions.InteractionSSEStreamEvent] { + if c == nil { + return nil + } + return c.InteractionSSEStreamEvent +} diff --git a/internal/sdk/models/operations/createwebhook.go b/internal/sdk/models/operations/createwebhook.go new file mode 100644 index 0000000..8a635ee --- /dev/null +++ b/internal/sdk/models/operations/createwebhook.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type CreateWebhookGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (c CreateWebhookGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateWebhookGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateWebhookGlobals) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateWebhookGlobals) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +type CreateWebhookRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The webhook to create. + Body webhooks.WebhookInput `request:"mediaType=application/json"` +} + +func (c CreateWebhookRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateWebhookRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateWebhookRequest) GetAPIRevision() *string { + if c == nil { + return nil + } + return c.APIRevision +} + +func (c *CreateWebhookRequest) GetAPIVersion() *string { + if c == nil { + return nil + } + return c.APIVersion +} + +func (c *CreateWebhookRequest) GetBody() webhooks.WebhookInput { + if c == nil { + return webhooks.WebhookInput{} + } + return c.Body +} + +type CreateWebhookResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Webhook *webhooks.Webhook +} + +func (c CreateWebhookResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *CreateWebhookResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *CreateWebhookResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CreateWebhookResponse) GetWebhook() *webhooks.Webhook { + if c == nil { + return nil + } + return c.Webhook +} diff --git a/internal/sdk/models/operations/deleteagent.go b/internal/sdk/models/operations/deleteagent.go new file mode 100644 index 0000000..c3f82bc --- /dev/null +++ b/internal/sdk/models/operations/deleteagent.go @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DeleteAgentGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (d DeleteAgentGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteAgentGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteAgentGlobals) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteAgentGlobals) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +type DeleteAgentRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d DeleteAgentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteAgentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteAgentRequest) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +func (d *DeleteAgentRequest) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteAgentRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteAgentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Empty *interactions.Empty +} + +func (d DeleteAgentResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteAgentResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteAgentResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeleteAgentResponse) GetEmpty() *interactions.Empty { + if d == nil { + return nil + } + return d.Empty +} diff --git a/internal/sdk/models/operations/deletecredential.go b/internal/sdk/models/operations/deletecredential.go new file mode 100644 index 0000000..f9a9cdf --- /dev/null +++ b/internal/sdk/models/operations/deletecredential.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DeleteCredentialGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (d DeleteCredentialGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteCredentialGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteCredentialGlobals) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +type DeleteCredentialRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d DeleteCredentialRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteCredentialRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteCredentialRequest) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteCredentialRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteCredentialResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Empty *interactions.Empty +} + +func (d DeleteCredentialResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteCredentialResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteCredentialResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeleteCredentialResponse) GetEmpty() *interactions.Empty { + if d == nil { + return nil + } + return d.Empty +} diff --git a/internal/sdk/models/operations/deleteenvironment.go b/internal/sdk/models/operations/deleteenvironment.go new file mode 100644 index 0000000..1cca8c1 --- /dev/null +++ b/internal/sdk/models/operations/deleteenvironment.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DeleteEnvironmentGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (d DeleteEnvironmentGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteEnvironmentGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteEnvironmentGlobals) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +type DeleteEnvironmentRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d DeleteEnvironmentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteEnvironmentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteEnvironmentRequest) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteEnvironmentRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteEnvironmentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Empty *interactions.Empty +} + +func (d DeleteEnvironmentResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteEnvironmentResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteEnvironmentResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeleteEnvironmentResponse) GetEmpty() *interactions.Empty { + if d == nil { + return nil + } + return d.Empty +} diff --git a/internal/sdk/models/operations/deleteinteraction.go b/internal/sdk/models/operations/deleteinteraction.go new file mode 100644 index 0000000..e0afea3 --- /dev/null +++ b/internal/sdk/models/operations/deleteinteraction.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DeleteInteractionGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (d DeleteInteractionGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteInteractionGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteInteractionGlobals) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteInteractionGlobals) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +type DeleteInteractionRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The unique identifier of the interaction to delete. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d DeleteInteractionRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteInteractionRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteInteractionRequest) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +func (d *DeleteInteractionRequest) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteInteractionRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteInteractionResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` +} + +func (d *DeleteInteractionResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} diff --git a/internal/sdk/models/operations/deletetrigger.go b/internal/sdk/models/operations/deletetrigger.go new file mode 100644 index 0000000..9885341 --- /dev/null +++ b/internal/sdk/models/operations/deletetrigger.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DeleteTriggerGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (d DeleteTriggerGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteTriggerGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteTriggerGlobals) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteTriggerGlobals) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +type DeleteTriggerRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource name of the trigger. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d DeleteTriggerRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteTriggerRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteTriggerRequest) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +func (d *DeleteTriggerRequest) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteTriggerRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteTriggerResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Empty *interactions.Empty +} + +func (d DeleteTriggerResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteTriggerResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteTriggerResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeleteTriggerResponse) GetEmpty() *interactions.Empty { + if d == nil { + return nil + } + return d.Empty +} diff --git a/internal/sdk/models/operations/deletewebhook.go b/internal/sdk/models/operations/deletewebhook.go new file mode 100644 index 0000000..c47f3ea --- /dev/null +++ b/internal/sdk/models/operations/deletewebhook.go @@ -0,0 +1,128 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type DeleteWebhookGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (d DeleteWebhookGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteWebhookGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteWebhookGlobals) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteWebhookGlobals) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +type DeleteWebhookRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The ID of the webhook to delete. + // Format: `{webhook_id}` + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d DeleteWebhookRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteWebhookRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteWebhookRequest) GetAPIRevision() *string { + if d == nil { + return nil + } + return d.APIRevision +} + +func (d *DeleteWebhookRequest) GetAPIVersion() *string { + if d == nil { + return nil + } + return d.APIVersion +} + +func (d *DeleteWebhookRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteWebhookResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Empty *interactions.Empty +} + +func (d DeleteWebhookResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(d, "", false) +} + +func (d *DeleteWebhookResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &d, "", false, nil); err != nil { + return err + } + return nil +} + +func (d *DeleteWebhookResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeleteWebhookResponse) GetEmpty() *interactions.Empty { + if d == nil { + return nil + } + return d.Empty +} diff --git a/internal/sdk/models/operations/filesdelete.go b/internal/sdk/models/operations/filesdelete.go new file mode 100644 index 0000000..662c2d7 --- /dev/null +++ b/internal/sdk/models/operations/filesdelete.go @@ -0,0 +1,108 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FilesDeleteGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (f FilesDeleteGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesDeleteGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesDeleteGlobals) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +type FilesDeleteRequest struct { + // Which version of the API to use. Defaults to v1beta. + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + File string `pathParam:"style=simple,explode=false,name=file"` +} + +func (f FilesDeleteRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesDeleteRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesDeleteRequest) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +func (f *FilesDeleteRequest) GetFile() string { + if f == nil { + return "" + } + return f.File +} + +type FilesDeleteResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Empty *genai.Empty +} + +func (f FilesDeleteResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesDeleteResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesDeleteResponse) GetHTTPMeta() components.HTTPMetadata { + if f == nil { + return components.HTTPMetadata{} + } + return f.HTTPMeta +} + +func (f *FilesDeleteResponse) GetEmpty() *genai.Empty { + if f == nil { + return nil + } + return f.Empty +} diff --git a/internal/sdk/models/operations/filesget.go b/internal/sdk/models/operations/filesget.go new file mode 100644 index 0000000..3e37157 --- /dev/null +++ b/internal/sdk/models/operations/filesget.go @@ -0,0 +1,108 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FilesGetGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (f FilesGetGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesGetGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesGetGlobals) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +type FilesGetRequest struct { + // Which version of the API to use. Defaults to v1beta. + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + File string `pathParam:"style=simple,explode=false,name=file"` +} + +func (f FilesGetRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesGetRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesGetRequest) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +func (f *FilesGetRequest) GetFile() string { + if f == nil { + return "" + } + return f.File +} + +type FilesGetResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + File *genai.File +} + +func (f FilesGetResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesGetResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesGetResponse) GetHTTPMeta() components.HTTPMetadata { + if f == nil { + return components.HTTPMetadata{} + } + return f.HTTPMeta +} + +func (f *FilesGetResponse) GetFile() *genai.File { + if f == nil { + return nil + } + return f.File +} diff --git a/internal/sdk/models/operations/fileslist.go b/internal/sdk/models/operations/fileslist.go new file mode 100644 index 0000000..beb1f73 --- /dev/null +++ b/internal/sdk/models/operations/fileslist.go @@ -0,0 +1,120 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FilesListGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (f FilesListGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesListGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesListGlobals) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +type FilesListRequest struct { + // Which version of the API to use. Defaults to v1beta. + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Optional. Maximum number of `File`s to return per page. If unspecified, defaults to 10. Maximum `page_size` is 100. + PageSize *int `queryParam:"style=form,explode=true,name=pageSize"` + // Optional. A page token from a previous `ListFiles` call. + PageToken *string `queryParam:"style=form,explode=true,name=pageToken"` +} + +func (f FilesListRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesListRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesListRequest) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +func (f *FilesListRequest) GetPageSize() *int { + if f == nil { + return nil + } + return f.PageSize +} + +func (f *FilesListRequest) GetPageToken() *string { + if f == nil { + return nil + } + return f.PageToken +} + +type FilesListResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + ListFilesResponse *genai.ListFilesResponse + + Next func() (*FilesListResponse, error) +} + +func (f FilesListResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesListResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesListResponse) GetHTTPMeta() components.HTTPMetadata { + if f == nil { + return components.HTTPMetadata{} + } + return f.HTTPMeta +} + +func (f *FilesListResponse) GetListFilesResponse() *genai.ListFilesResponse { + if f == nil { + return nil + } + return f.ListFilesResponse +} diff --git a/internal/sdk/models/operations/filesregister.go b/internal/sdk/models/operations/filesregister.go new file mode 100644 index 0000000..80809db --- /dev/null +++ b/internal/sdk/models/operations/filesregister.go @@ -0,0 +1,108 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type FilesRegisterGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (f FilesRegisterGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesRegisterGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesRegisterGlobals) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +type FilesRegisterRequest struct { + // Which version of the API to use. Defaults to v1beta. + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + Body genai.RegisterFilesRequest `request:"mediaType=application/json"` +} + +func (f FilesRegisterRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesRegisterRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesRegisterRequest) GetAPIVersion() *string { + if f == nil { + return nil + } + return f.APIVersion +} + +func (f *FilesRegisterRequest) GetBody() genai.RegisterFilesRequest { + if f == nil { + return genai.RegisterFilesRequest{} + } + return f.Body +} + +type FilesRegisterResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + RegisterFilesResponse *genai.RegisterFilesResponse +} + +func (f FilesRegisterResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(f, "", false) +} + +func (f *FilesRegisterResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &f, "", false, nil); err != nil { + return err + } + return nil +} + +func (f *FilesRegisterResponse) GetHTTPMeta() components.HTTPMetadata { + if f == nil { + return components.HTTPMetadata{} + } + return f.HTTPMeta +} + +func (f *FilesRegisterResponse) GetRegisterFilesResponse() *genai.RegisterFilesResponse { + if f == nil { + return nil + } + return f.RegisterFilesResponse +} diff --git a/internal/sdk/models/operations/getagent.go b/internal/sdk/models/operations/getagent.go new file mode 100644 index 0000000..dfe921f --- /dev/null +++ b/internal/sdk/models/operations/getagent.go @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/agents" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GetAgentGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (g GetAgentGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetAgentGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetAgentGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetAgentGlobals) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +type GetAgentRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (g GetAgentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetAgentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetAgentRequest) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +func (g *GetAgentRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetAgentRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +type GetAgentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Agent *agents.Agent +} + +func (g GetAgentResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetAgentResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetAgentResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetAgentResponse) GetAgent() *agents.Agent { + if g == nil { + return nil + } + return g.Agent +} diff --git a/internal/sdk/models/operations/getcredential.go b/internal/sdk/models/operations/getcredential.go new file mode 100644 index 0000000..df6c070 --- /dev/null +++ b/internal/sdk/models/operations/getcredential.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/credentials" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GetCredentialGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (g GetCredentialGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetCredentialGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetCredentialGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +type GetCredentialRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (g GetCredentialRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetCredentialRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetCredentialRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetCredentialRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +type GetCredentialResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Credential *credentials.Credential +} + +func (g GetCredentialResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetCredentialResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetCredentialResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetCredentialResponse) GetCredential() *credentials.Credential { + if g == nil { + return nil + } + return g.Credential +} diff --git a/internal/sdk/models/operations/getenvironment.go b/internal/sdk/models/operations/getenvironment.go new file mode 100644 index 0000000..2b5013c --- /dev/null +++ b/internal/sdk/models/operations/getenvironment.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/environments" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GetEnvironmentGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (g GetEnvironmentGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +type GetEnvironmentRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (g GetEnvironmentRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetEnvironmentRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +type GetEnvironmentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Environment *environments.Environment +} + +func (g GetEnvironmentResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetEnvironmentResponse) GetEnvironment() *environments.Environment { + if g == nil { + return nil + } + return g.Environment +} diff --git a/internal/sdk/models/operations/getenvironmentfiles.go b/internal/sdk/models/operations/getenvironmentfiles.go new file mode 100644 index 0000000..fa04259 --- /dev/null +++ b/internal/sdk/models/operations/getenvironmentfiles.go @@ -0,0 +1,143 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/environments" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GetEnvironmentFilesGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (g GetEnvironmentFilesGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentFilesGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentFilesGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +type GetEnvironmentFilesRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + Environment string `pathParam:"style=simple,explode=false,name=environment"` + Path string `pathParam:"style=simple,explode=false,name=path"` + // Optional. Maximum number of entries to return per page (for directory listing). + PageSize *int `queryParam:"style=form,explode=true,name=page_size"` + // Optional. Pagination token for directory listing. + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` + // Optional. If true and the path is a directory, recursively lists all files. + Recursive *bool `queryParam:"style=form,explode=true,name=recursive"` +} + +func (g GetEnvironmentFilesRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentFilesRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentFilesRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetEnvironmentFilesRequest) GetEnvironment() string { + if g == nil { + return "" + } + return g.Environment +} + +func (g *GetEnvironmentFilesRequest) GetPath() string { + if g == nil { + return "" + } + return g.Path +} + +func (g *GetEnvironmentFilesRequest) GetPageSize() *int { + if g == nil { + return nil + } + return g.PageSize +} + +func (g *GetEnvironmentFilesRequest) GetPageToken() *string { + if g == nil { + return nil + } + return g.PageToken +} + +func (g *GetEnvironmentFilesRequest) GetRecursive() *bool { + if g == nil { + return nil + } + return g.Recursive +} + +type GetEnvironmentFilesResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + GetEnvironmentFilesResponse *environments.GetEnvironmentFilesResponse +} + +func (g GetEnvironmentFilesResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetEnvironmentFilesResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetEnvironmentFilesResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetEnvironmentFilesResponse) GetGetEnvironmentFilesResponse() *environments.GetEnvironmentFilesResponse { + if g == nil { + return nil + } + return g.GetEnvironmentFilesResponse +} diff --git a/internal/sdk/models/operations/getinteractionbyid.go b/internal/sdk/models/operations/getinteractionbyid.go new file mode 100644 index 0000000..7e3a7af --- /dev/null +++ b/internal/sdk/models/operations/getinteractionbyid.go @@ -0,0 +1,166 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types/stream" +) + +type GetInteractionByIDGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (g GetInteractionByIDGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetInteractionByIDGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetInteractionByIDGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetInteractionByIDGlobals) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +type GetInteractionByIDRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The unique identifier of the interaction to retrieve. + ID string `pathParam:"style=simple,explode=false,name=id"` + // If set to true, includes the input in the response. + // + // Deprecated: This will be removed in a future release, please migrate away from it as soon as possible. + IncludeInput *bool `default:"false" queryParam:"style=form,explode=true,name=include_input"` + // Optional. If set, resumes the interaction stream from the next chunk after the event marked by the event id. Can only be used if `stream` is true. + LastEventID *string `queryParam:"style=form,explode=true,name=last_event_id"` + // Stream the interaction's events (replayed from the start for a finished interaction) instead of returning the status object. Defaults to true; use --stream=false for the status object. + Stream *bool `default:"true" queryParam:"style=form,explode=true,name=stream"` +} + +func (g GetInteractionByIDRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetInteractionByIDRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetInteractionByIDRequest) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +func (g *GetInteractionByIDRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetInteractionByIDRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +func (g *GetInteractionByIDRequest) GetIncludeInput() *bool { + if g == nil { + return nil + } + return g.IncludeInput +} + +func (g *GetInteractionByIDRequest) GetLastEventID() *string { + if g == nil { + return nil + } + return g.LastEventID +} + +func (g *GetInteractionByIDRequest) GetStream() *bool { + if g == nil { + return nil + } + return g.Stream +} + +type GetInteractionByIDResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful retrieval of the interaction. + Interaction *interactions.Interaction + // Successful retrieval of the interaction. + InteractionSSEStreamEvent *stream.EventStream[interactions.InteractionSSEStreamEvent] +} + +func (g GetInteractionByIDResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetInteractionByIDResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetInteractionByIDResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetInteractionByIDResponse) GetInteraction() *interactions.Interaction { + if g == nil { + return nil + } + return g.Interaction +} + +func (g *GetInteractionByIDResponse) GetInteractionSSEStreamEvent() *stream.EventStream[interactions.InteractionSSEStreamEvent] { + if g == nil { + return nil + } + return g.InteractionSSEStreamEvent +} diff --git a/internal/sdk/models/operations/gettrigger.go b/internal/sdk/models/operations/gettrigger.go new file mode 100644 index 0000000..ebf3e45 --- /dev/null +++ b/internal/sdk/models/operations/gettrigger.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/triggers" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GetTriggerGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (g GetTriggerGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetTriggerGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetTriggerGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetTriggerGlobals) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +type GetTriggerRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource name of the trigger. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (g GetTriggerRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetTriggerRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetTriggerRequest) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +func (g *GetTriggerRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetTriggerRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +type GetTriggerResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Trigger *triggers.Trigger +} + +func (g GetTriggerResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetTriggerResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetTriggerResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetTriggerResponse) GetTrigger() *triggers.Trigger { + if g == nil { + return nil + } + return g.Trigger +} diff --git a/internal/sdk/models/operations/getwebhook.go b/internal/sdk/models/operations/getwebhook.go new file mode 100644 index 0000000..e2a282a --- /dev/null +++ b/internal/sdk/models/operations/getwebhook.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type GetWebhookGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (g GetWebhookGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetWebhookGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetWebhookGlobals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetWebhookGlobals) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +type GetWebhookRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The ID of the webhook to retrieve. + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (g GetWebhookRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetWebhookRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetWebhookRequest) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +func (g *GetWebhookRequest) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *GetWebhookRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +type GetWebhookResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Webhook *webhooks.Webhook +} + +func (g GetWebhookResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *GetWebhookResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *GetWebhookResponse) GetHTTPMeta() components.HTTPMetadata { + if g == nil { + return components.HTTPMetadata{} + } + return g.HTTPMeta +} + +func (g *GetWebhookResponse) GetWebhook() *webhooks.Webhook { + if g == nil { + return nil + } + return g.Webhook +} diff --git a/internal/sdk/models/operations/listagents.go b/internal/sdk/models/operations/listagents.go new file mode 100644 index 0000000..a931fac --- /dev/null +++ b/internal/sdk/models/operations/listagents.go @@ -0,0 +1,144 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/agents" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ListAgentsGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (l ListAgentsGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListAgentsGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListAgentsGlobals) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListAgentsGlobals) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +type ListAgentsRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + PageSize *int `queryParam:"style=form,explode=true,name=page_size"` + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` + Parent *string `queryParam:"style=form,explode=true,name=parent"` +} + +func (l ListAgentsRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListAgentsRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListAgentsRequest) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +func (l *ListAgentsRequest) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListAgentsRequest) GetPageSize() *int { + if l == nil { + return nil + } + return l.PageSize +} + +func (l *ListAgentsRequest) GetPageToken() *string { + if l == nil { + return nil + } + return l.PageToken +} + +func (l *ListAgentsRequest) GetParent() *string { + if l == nil { + return nil + } + return l.Parent +} + +type ListAgentsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + AgentListResponse *agents.AgentListResponse + + Next func() (*ListAgentsResponse, error) +} + +func (l ListAgentsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListAgentsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListAgentsResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListAgentsResponse) GetAgentListResponse() *agents.AgentListResponse { + if l == nil { + return nil + } + return l.AgentListResponse +} diff --git a/internal/sdk/models/operations/listcredentials.go b/internal/sdk/models/operations/listcredentials.go new file mode 100644 index 0000000..4431b2d --- /dev/null +++ b/internal/sdk/models/operations/listcredentials.go @@ -0,0 +1,119 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/credentials" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ListCredentialsGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (l ListCredentialsGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListCredentialsGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListCredentialsGlobals) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +type ListCredentialsRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Optional. Maximum number of credentials to return. + // If unspecified, defaults to 50. Maximum is 1000. + PageSize *int `queryParam:"style=form,explode=true,name=page_size"` + // Optional. Pagination token. + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` +} + +func (l ListCredentialsRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListCredentialsRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListCredentialsRequest) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListCredentialsRequest) GetPageSize() *int { + if l == nil { + return nil + } + return l.PageSize +} + +func (l *ListCredentialsRequest) GetPageToken() *string { + if l == nil { + return nil + } + return l.PageToken +} + +type ListCredentialsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + CredentialListResponse *credentials.CredentialListResponse +} + +func (l ListCredentialsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListCredentialsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListCredentialsResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListCredentialsResponse) GetCredentialListResponse() *credentials.CredentialListResponse { + if l == nil { + return nil + } + return l.CredentialListResponse +} diff --git a/internal/sdk/models/operations/listenvironments.go b/internal/sdk/models/operations/listenvironments.go new file mode 100644 index 0000000..87dc204 --- /dev/null +++ b/internal/sdk/models/operations/listenvironments.go @@ -0,0 +1,118 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/environments" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ListEnvironmentsGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (l ListEnvironmentsGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListEnvironmentsGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListEnvironmentsGlobals) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +type ListEnvironmentsRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Optional. Maximum number of environments to return.\nIf unspecified, defaults to 50. Maximum is 1000. + PageSize *int `queryParam:"style=form,explode=true,name=page_size"` + // Optional. Pagination token. + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` +} + +func (l ListEnvironmentsRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListEnvironmentsRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListEnvironmentsRequest) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListEnvironmentsRequest) GetPageSize() *int { + if l == nil { + return nil + } + return l.PageSize +} + +func (l *ListEnvironmentsRequest) GetPageToken() *string { + if l == nil { + return nil + } + return l.PageToken +} + +type ListEnvironmentsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + ListEnvironmentsResponse *environments.ListEnvironmentsResponse +} + +func (l ListEnvironmentsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListEnvironmentsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListEnvironmentsResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListEnvironmentsResponse) GetListEnvironmentsResponse() *environments.ListEnvironmentsResponse { + if l == nil { + return nil + } + return l.ListEnvironmentsResponse +} diff --git a/internal/sdk/models/operations/listtriggerexecutions.go b/internal/sdk/models/operations/listtriggerexecutions.go new file mode 100644 index 0000000..0a7fa15 --- /dev/null +++ b/internal/sdk/models/operations/listtriggerexecutions.go @@ -0,0 +1,147 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/triggers" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ListTriggerExecutionsGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (l ListTriggerExecutionsGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggerExecutionsGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggerExecutionsGlobals) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListTriggerExecutionsGlobals) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +type ListTriggerExecutionsRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource name of the trigger. + TriggerID string `pathParam:"style=simple,explode=false,name=trigger_id"` + // Optional. The maximum number of executions to return per page. + PageSize *int64 `queryParam:"style=form,explode=true,name=page_size"` + // Optional. A page token from a previous ListTriggerExecutions call. + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` +} + +func (l ListTriggerExecutionsRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggerExecutionsRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggerExecutionsRequest) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +func (l *ListTriggerExecutionsRequest) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListTriggerExecutionsRequest) GetTriggerID() string { + if l == nil { + return "" + } + return l.TriggerID +} + +func (l *ListTriggerExecutionsRequest) GetPageSize() *int64 { + if l == nil { + return nil + } + return l.PageSize +} + +func (l *ListTriggerExecutionsRequest) GetPageToken() *string { + if l == nil { + return nil + } + return l.PageToken +} + +type ListTriggerExecutionsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + ListTriggerExecutionsResponse *triggers.ListTriggerExecutionsResponse + + Next func() (*ListTriggerExecutionsResponse, error) +} + +func (l ListTriggerExecutionsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggerExecutionsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggerExecutionsResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListTriggerExecutionsResponse) GetListTriggerExecutionsResponse() *triggers.ListTriggerExecutionsResponse { + if l == nil { + return nil + } + return l.ListTriggerExecutionsResponse +} diff --git a/internal/sdk/models/operations/listtriggers.go b/internal/sdk/models/operations/listtriggers.go new file mode 100644 index 0000000..9a0818e --- /dev/null +++ b/internal/sdk/models/operations/listtriggers.go @@ -0,0 +1,147 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/triggers" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ListTriggersGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (l ListTriggersGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggersGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggersGlobals) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListTriggersGlobals) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +type ListTriggersRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Optional. Filter expression (e.g., by state). + Filter *string `queryParam:"style=form,explode=true,name=filter"` + // Optional. The maximum number of triggers to return per page. + PageSize *int64 `queryParam:"style=form,explode=true,name=page_size"` + // Optional. A page token from a previous ListTriggers call. + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` +} + +func (l ListTriggersRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggersRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggersRequest) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +func (l *ListTriggersRequest) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListTriggersRequest) GetFilter() *string { + if l == nil { + return nil + } + return l.Filter +} + +func (l *ListTriggersRequest) GetPageSize() *int64 { + if l == nil { + return nil + } + return l.PageSize +} + +func (l *ListTriggersRequest) GetPageToken() *string { + if l == nil { + return nil + } + return l.PageToken +} + +type ListTriggersResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + ListTriggersResponse *triggers.ListTriggersResponse + + Next func() (*ListTriggersResponse, error) +} + +func (l ListTriggersResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggersResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggersResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListTriggersResponse) GetListTriggersResponse() *triggers.ListTriggersResponse { + if l == nil { + return nil + } + return l.ListTriggersResponse +} diff --git a/internal/sdk/models/operations/listwebhooks.go b/internal/sdk/models/operations/listwebhooks.go new file mode 100644 index 0000000..79bca3f --- /dev/null +++ b/internal/sdk/models/operations/listwebhooks.go @@ -0,0 +1,141 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ListWebhooksGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (l ListWebhooksGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListWebhooksGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListWebhooksGlobals) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListWebhooksGlobals) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +type ListWebhooksRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Optional. The maximum number of webhooks to return. The service may return fewer than + // this value. If unspecified, at most 50 webhooks will be returned. + // The maximum value is 1000. + PageSize *int `queryParam:"style=form,explode=true,name=page_size"` + // Optional. A page token, received from a previous `ListWebhooks` call. + // Provide this to retrieve the subsequent page. + PageToken *string `queryParam:"style=form,explode=true,name=page_token"` +} + +func (l ListWebhooksRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListWebhooksRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListWebhooksRequest) GetAPIRevision() *string { + if l == nil { + return nil + } + return l.APIRevision +} + +func (l *ListWebhooksRequest) GetAPIVersion() *string { + if l == nil { + return nil + } + return l.APIVersion +} + +func (l *ListWebhooksRequest) GetPageSize() *int { + if l == nil { + return nil + } + return l.PageSize +} + +func (l *ListWebhooksRequest) GetPageToken() *string { + if l == nil { + return nil + } + return l.PageToken +} + +type ListWebhooksResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + WebhookListResponse *webhooks.WebhookListResponse + + Next func() (*ListWebhooksResponse, error) +} + +func (l ListWebhooksResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListWebhooksResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListWebhooksResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListWebhooksResponse) GetWebhookListResponse() *webhooks.WebhookListResponse { + if l == nil { + return nil + } + return l.WebhookListResponse +} diff --git a/internal/sdk/models/operations/modelsget.go b/internal/sdk/models/operations/modelsget.go new file mode 100644 index 0000000..65dd195 --- /dev/null +++ b/internal/sdk/models/operations/modelsget.go @@ -0,0 +1,108 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ModelsGetGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (m ModelsGetGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelsGetGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelsGetGlobals) GetAPIVersion() *string { + if m == nil { + return nil + } + return m.APIVersion +} + +type ModelsGetRequest struct { + // Which version of the API to use. Defaults to v1beta. + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + Model string `pathParam:"style=simple,explode=false,name=model"` +} + +func (m ModelsGetRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelsGetRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelsGetRequest) GetAPIVersion() *string { + if m == nil { + return nil + } + return m.APIVersion +} + +func (m *ModelsGetRequest) GetModel() string { + if m == nil { + return "" + } + return m.Model +} + +type ModelsGetResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Model *genai.Model +} + +func (m ModelsGetResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelsGetResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelsGetResponse) GetHTTPMeta() components.HTTPMetadata { + if m == nil { + return components.HTTPMetadata{} + } + return m.HTTPMeta +} + +func (m *ModelsGetResponse) GetModel() *genai.Model { + if m == nil { + return nil + } + return m.Model +} diff --git a/internal/sdk/models/operations/modelslist.go b/internal/sdk/models/operations/modelslist.go new file mode 100644 index 0000000..6f6a158 --- /dev/null +++ b/internal/sdk/models/operations/modelslist.go @@ -0,0 +1,120 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/genai" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type ModelsListGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (m ModelsListGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelsListGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelsListGlobals) GetAPIVersion() *string { + if m == nil { + return nil + } + return m.APIVersion +} + +type ModelsListRequest struct { + // Which version of the API to use. Defaults to v1beta. + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The maximum number of `Models` to return (per page). If unspecified, 50 models will be returned per page. This method returns at most 1000 models per page, even if you pass a larger page_size. + PageSize *int `queryParam:"style=form,explode=true,name=pageSize"` + // A page token, received from a previous `ListModels` call. Provide the `page_token` returned by one request as an argument to the next request to retrieve the next page. When paginating, all other parameters provided to `ListModels` must match the call that provided the page token. + PageToken *string `queryParam:"style=form,explode=true,name=pageToken"` +} + +func (m ModelsListRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelsListRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelsListRequest) GetAPIVersion() *string { + if m == nil { + return nil + } + return m.APIVersion +} + +func (m *ModelsListRequest) GetPageSize() *int { + if m == nil { + return nil + } + return m.PageSize +} + +func (m *ModelsListRequest) GetPageToken() *string { + if m == nil { + return nil + } + return m.PageToken +} + +type ModelsListResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + ListModelsResponse *genai.ListModelsResponse + + Next func() (*ModelsListResponse, error) +} + +func (m ModelsListResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(m, "", false) +} + +func (m *ModelsListResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &m, "", false, nil); err != nil { + return err + } + return nil +} + +func (m *ModelsListResponse) GetHTTPMeta() components.HTTPMetadata { + if m == nil { + return components.HTTPMetadata{} + } + return m.HTTPMeta +} + +func (m *ModelsListResponse) GetListModelsResponse() *genai.ListModelsResponse { + if m == nil { + return nil + } + return m.ListModelsResponse +} diff --git a/internal/sdk/models/operations/options.go b/internal/sdk/models/operations/options.go new file mode 100644 index 0000000..26a7932 --- /dev/null +++ b/internal/sdk/models/operations/options.go @@ -0,0 +1,152 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "errors" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +var ErrUnsupportedOption = errors.New("unsupported option") + +const ( + SupportedOptionRetries = "retries" + SupportedOptionTimeout = "timeout" + SupportedOptionAcceptHeaderOverride = "acceptHeaderOverride" + SupportedOptionURLOverride = "urlOverride" + SupportedOptionSkipDeserialization = "skipDeserialization" +) + +type AcceptHeaderEnum string + +const ( + AcceptHeaderEnumApplicationJson AcceptHeaderEnum = "application/json" + AcceptHeaderEnumWildcardRootWildcard AcceptHeaderEnum = "*/*" + AcceptHeaderEnumTextEventStream AcceptHeaderEnum = "text/event-stream" +) + +func (e AcceptHeaderEnum) ToPointer() *AcceptHeaderEnum { + return &e +} + +type Options struct { + ServerURL *string + Retries *retry.Config + Timeout *time.Duration + AcceptHeaderOverride *AcceptHeaderEnum + URLOverride *string + SetHeaders map[string]string + SkipDeserialization *bool +} + +type Option func(*Options, ...string) error + +// WithServerURL allows providing an alternative server URL. +func WithServerURL(serverURL string) Option { + return func(opts *Options, supportedOptions ...string) error { + opts.ServerURL = &serverURL + return nil + } +} + +// WithTemplatedServerURL allows providing an alternative server URL with templated parameters. +func WithTemplatedServerURL(serverURL string, params map[string]string) Option { + return func(opts *Options, supportedOptions ...string) error { + if params != nil { + serverURL = utils.ReplaceParameters(serverURL, params) + } + + opts.ServerURL = &serverURL + return nil + } +} + +// WithRetries allows customizing the default retry configuration. +func WithRetries(config retry.Config) Option { + return func(opts *Options, supportedOptions ...string) error { + if !utils.Contains(supportedOptions, SupportedOptionRetries) { + return ErrUnsupportedOption + } + + opts.Retries = &config + return nil + } +} + +// WithOperationTimeout allows setting the request timeout applied for an operation. +func WithOperationTimeout(timeout time.Duration) Option { + return func(opts *Options, supportedOptions ...string) error { + if !utils.Contains(supportedOptions, SupportedOptionTimeout) { + return ErrUnsupportedOption + } + + opts.Timeout = &timeout + return nil + } +} + +func WithAcceptHeaderOverride(acceptHeaderOverride AcceptHeaderEnum) Option { + return func(opts *Options, supportedOptions ...string) error { + if !utils.Contains(supportedOptions, SupportedOptionAcceptHeaderOverride) { + return ErrUnsupportedOption + } + + opts.AcceptHeaderOverride = &acceptHeaderOverride + return nil + } +} + +// WithURLOverride allows overriding the URL. +func WithURLOverride(urlOverride string) Option { + return func(opts *Options, supportedOptions ...string) error { + if !utils.Contains(supportedOptions, SupportedOptionURLOverride) { + return ErrUnsupportedOption + } + + opts.URLOverride = &urlOverride + return nil + } +} + +// WithSetHeaders takes a map of headers that will applied to a request. If the +// request contains headers that are in the map then they will be overwritten. +func WithSetHeaders(hdrs map[string]string) Option { + return func(opts *Options, supportedOptions ...string) error { + opts.SetHeaders = hdrs + return nil + } +} + +// WithSkipDeserialization skips typed deserialization of successful JSON responses. The +// body is still consumed while the operation context is alive and replayed on +// the HTTP response, so the caller can read it via the HTTPMeta.Response field +// after the method returns. Non-JSON responses and error responses are always +// deserialized regardless of this option. +func WithSkipDeserialization() Option { + return func(opts *Options, supportedOptions ...string) error { + if !utils.Contains(supportedOptions, SupportedOptionSkipDeserialization) { + return ErrUnsupportedOption + } + + t := true + opts.SkipDeserialization = &t + return nil + } +} diff --git a/internal/sdk/models/operations/pingwebhook.go b/internal/sdk/models/operations/pingwebhook.go new file mode 100644 index 0000000..0951561 --- /dev/null +++ b/internal/sdk/models/operations/pingwebhook.go @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type PingWebhookGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (p PingWebhookGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *PingWebhookGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *PingWebhookGlobals) GetAPIVersion() *string { + if p == nil { + return nil + } + return p.APIVersion +} + +func (p *PingWebhookGlobals) GetAPIRevision() *string { + if p == nil { + return nil + } + return p.APIRevision +} + +type PingWebhookRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The ID of the webhook to ping. + // Format: `{webhook_id}` + ID string `pathParam:"style=simple,explode=false,name=id"` + // The request body. + Body *webhooks.PingWebhookRequest `request:"mediaType=application/json"` +} + +func (p PingWebhookRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *PingWebhookRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *PingWebhookRequest) GetAPIRevision() *string { + if p == nil { + return nil + } + return p.APIRevision +} + +func (p *PingWebhookRequest) GetAPIVersion() *string { + if p == nil { + return nil + } + return p.APIVersion +} + +func (p *PingWebhookRequest) GetID() string { + if p == nil { + return "" + } + return p.ID +} + +func (p *PingWebhookRequest) GetBody() *webhooks.PingWebhookRequest { + if p == nil { + return nil + } + return p.Body +} + +type PingWebhookResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + WebhookPingResponse *webhooks.WebhookPingResponse +} + +func (p PingWebhookResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(p, "", false) +} + +func (p *PingWebhookResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &p, "", false, nil); err != nil { + return err + } + return nil +} + +func (p *PingWebhookResponse) GetHTTPMeta() components.HTTPMetadata { + if p == nil { + return components.HTTPMetadata{} + } + return p.HTTPMeta +} + +func (p *PingWebhookResponse) GetWebhookPingResponse() *webhooks.WebhookPingResponse { + if p == nil { + return nil + } + return p.WebhookPingResponse +} diff --git a/internal/sdk/models/operations/rotatesigningsecret.go b/internal/sdk/models/operations/rotatesigningsecret.go new file mode 100644 index 0000000..96f4f8b --- /dev/null +++ b/internal/sdk/models/operations/rotatesigningsecret.go @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type RotateSigningSecretGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (r RotateSigningSecretGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RotateSigningSecretGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RotateSigningSecretGlobals) GetAPIVersion() *string { + if r == nil { + return nil + } + return r.APIVersion +} + +func (r *RotateSigningSecretGlobals) GetAPIRevision() *string { + if r == nil { + return nil + } + return r.APIRevision +} + +type RotateSigningSecretRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The ID of the webhook for which to generate a signing secret. + // Format: `{webhook_id}` + ID string `pathParam:"style=simple,explode=false,name=id"` + // The request body. + Body *webhooks.RotateSigningSecretRequest `request:"mediaType=application/json"` +} + +func (r RotateSigningSecretRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RotateSigningSecretRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RotateSigningSecretRequest) GetAPIRevision() *string { + if r == nil { + return nil + } + return r.APIRevision +} + +func (r *RotateSigningSecretRequest) GetAPIVersion() *string { + if r == nil { + return nil + } + return r.APIVersion +} + +func (r *RotateSigningSecretRequest) GetID() string { + if r == nil { + return "" + } + return r.ID +} + +func (r *RotateSigningSecretRequest) GetBody() *webhooks.RotateSigningSecretRequest { + if r == nil { + return nil + } + return r.Body +} + +type RotateSigningSecretResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + WebhookRotateSigningSecretResponse *webhooks.WebhookRotateSigningSecretResponse +} + +func (r RotateSigningSecretResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RotateSigningSecretResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RotateSigningSecretResponse) GetHTTPMeta() components.HTTPMetadata { + if r == nil { + return components.HTTPMetadata{} + } + return r.HTTPMeta +} + +func (r *RotateSigningSecretResponse) GetWebhookRotateSigningSecretResponse() *webhooks.WebhookRotateSigningSecretResponse { + if r == nil { + return nil + } + return r.WebhookRotateSigningSecretResponse +} diff --git a/internal/sdk/models/operations/runtrigger.go b/internal/sdk/models/operations/runtrigger.go new file mode 100644 index 0000000..9ac6a96 --- /dev/null +++ b/internal/sdk/models/operations/runtrigger.go @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/triggers" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type RunTriggerGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (r RunTriggerGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RunTriggerGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RunTriggerGlobals) GetAPIVersion() *string { + if r == nil { + return nil + } + return r.APIVersion +} + +func (r *RunTriggerGlobals) GetAPIRevision() *string { + if r == nil { + return nil + } + return r.APIRevision +} + +type RunTriggerRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource name of the trigger. + TriggerID string `pathParam:"style=simple,explode=false,name=trigger_id"` +} + +func (r RunTriggerRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RunTriggerRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RunTriggerRequest) GetAPIRevision() *string { + if r == nil { + return nil + } + return r.APIRevision +} + +func (r *RunTriggerRequest) GetAPIVersion() *string { + if r == nil { + return nil + } + return r.APIVersion +} + +func (r *RunTriggerRequest) GetTriggerID() string { + if r == nil { + return "" + } + return r.TriggerID +} + +type RunTriggerResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + TriggerExecution *triggers.TriggerExecution +} + +func (r RunTriggerResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(r, "", false) +} + +func (r *RunTriggerResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { + return err + } + return nil +} + +func (r *RunTriggerResponse) GetHTTPMeta() components.HTTPMetadata { + if r == nil { + return components.HTTPMetadata{} + } + return r.HTTPMeta +} + +func (r *RunTriggerResponse) GetTriggerExecution() *triggers.TriggerExecution { + if r == nil { + return nil + } + return r.TriggerExecution +} diff --git a/internal/sdk/models/operations/startenvironmentfileupload.go b/internal/sdk/models/operations/startenvironmentfileupload.go new file mode 100644 index 0000000..bc2e73e --- /dev/null +++ b/internal/sdk/models/operations/startenvironmentfileupload.go @@ -0,0 +1,155 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type StartEnvironmentFileUploadGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (s StartEnvironmentFileUploadGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StartEnvironmentFileUploadGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StartEnvironmentFileUploadGlobals) GetAPIVersion() *string { + if s == nil { + return nil + } + return s.APIVersion +} + +type StartEnvironmentFileUploadRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // The ID of the environment that owns the destination file. + Environment string `pathParam:"style=simple,explode=false,name=environment"` + // The relative destination path inside the environment workspace. + Path string `pathParam:"style=simple,explode=false,name=path"` + // Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`. + Extract *bool `queryParam:"style=form,explode=true,name=extract"` + // Optional. Whether to overwrite the destination file if it already exists. + Overwrite *bool `queryParam:"style=form,explode=true,name=overwrite"` + // Command that starts the resumable upload session. + //lint:ignore U1000 accessed via reflection for JSON marshaling + xGoogUploadCommand string `const:"start" header:"style=simple,explode=false,name=X-Goog-Upload-Command"` + // Total number of file bytes that will be uploaded to the session URL. + XGoogUploadHeaderContentLength int64 `header:"style=simple,explode=false,name=X-Goog-Upload-Header-Content-Length"` + // MIME type of the file that will be uploaded to the session URL. + XGoogUploadHeaderContentType string `header:"style=simple,explode=false,name=X-Goog-Upload-Header-Content-Type"` + // Resumable upload protocol selector. + //lint:ignore U1000 accessed via reflection for JSON marshaling + xGoogUploadProtocol string `const:"resumable" header:"style=simple,explode=false,name=X-Goog-Upload-Protocol"` +} + +func (s StartEnvironmentFileUploadRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *StartEnvironmentFileUploadRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *StartEnvironmentFileUploadRequest) GetAPIVersion() *string { + if s == nil { + return nil + } + return s.APIVersion +} + +func (s *StartEnvironmentFileUploadRequest) GetEnvironment() string { + if s == nil { + return "" + } + return s.Environment +} + +func (s *StartEnvironmentFileUploadRequest) GetPath() string { + if s == nil { + return "" + } + return s.Path +} + +func (s *StartEnvironmentFileUploadRequest) GetExtract() *bool { + if s == nil { + return nil + } + return s.Extract +} + +func (s *StartEnvironmentFileUploadRequest) GetOverwrite() *bool { + if s == nil { + return nil + } + return s.Overwrite +} + +func (s *StartEnvironmentFileUploadRequest) GetXGoogUploadCommand() string { + return "start" +} + +func (s *StartEnvironmentFileUploadRequest) GetXGoogUploadHeaderContentLength() int64 { + if s == nil { + return 0 + } + return s.XGoogUploadHeaderContentLength +} + +func (s *StartEnvironmentFileUploadRequest) GetXGoogUploadHeaderContentType() string { + if s == nil { + return "" + } + return s.XGoogUploadHeaderContentType +} + +func (s *StartEnvironmentFileUploadRequest) GetXGoogUploadProtocol() string { + return "resumable" +} + +type StartEnvironmentFileUploadResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + Headers map[string][]string +} + +func (s *StartEnvironmentFileUploadResponse) GetHTTPMeta() components.HTTPMetadata { + if s == nil { + return components.HTTPMetadata{} + } + return s.HTTPMeta +} + +func (s *StartEnvironmentFileUploadResponse) GetHeaders() map[string][]string { + if s == nil { + return map[string][]string{} + } + return s.Headers +} diff --git a/internal/sdk/models/operations/updatecredential.go b/internal/sdk/models/operations/updatecredential.go new file mode 100644 index 0000000..721822d --- /dev/null +++ b/internal/sdk/models/operations/updatecredential.go @@ -0,0 +1,138 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/credentials" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type UpdateCredentialGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` +} + +func (u UpdateCredentialGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateCredentialGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateCredentialGlobals) GetAPIVersion() *string { + if u == nil { + return nil + } + return u.APIVersion +} + +type UpdateCredentialRequest struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + ID string `pathParam:"style=simple,explode=false,name=id"` + // Optional. The list of fields to update. + UpdateMask *string `queryParam:"style=form,explode=true,name=update_mask"` + Body credentials.CredentialUpdate `request:"mediaType=application/json"` +} + +func (u UpdateCredentialRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateCredentialRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateCredentialRequest) GetAPIVersion() *string { + if u == nil { + return nil + } + return u.APIVersion +} + +func (u *UpdateCredentialRequest) GetID() string { + if u == nil { + return "" + } + return u.ID +} + +func (u *UpdateCredentialRequest) GetUpdateMask() *string { + if u == nil { + return nil + } + return u.UpdateMask +} + +func (u *UpdateCredentialRequest) GetBody() credentials.CredentialUpdate { + if u == nil { + return credentials.CredentialUpdate{} + } + return u.Body +} + +func (u *UpdateCredentialRequest) GetBodyEnvironmentVariable() *credentials.EnvironmentVariableUpdateConfig { + return u.GetBody().EnvironmentVariableUpdateConfig +} + +func (u *UpdateCredentialRequest) GetBodyBearerToken() *credentials.HTTPBearerUpdateConfig { + return u.GetBody().HTTPBearerUpdateConfig +} + +func (u *UpdateCredentialRequest) GetBodyOauth2() *credentials.OAuth2UpdateConfig { + return u.GetBody().OAuth2UpdateConfig +} + +type UpdateCredentialResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Credential *credentials.Credential +} + +func (u UpdateCredentialResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateCredentialResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateCredentialResponse) GetHTTPMeta() components.HTTPMetadata { + if u == nil { + return components.HTTPMetadata{} + } + return u.HTTPMeta +} + +func (u *UpdateCredentialResponse) GetCredential() *credentials.Credential { + if u == nil { + return nil + } + return u.Credential +} diff --git a/internal/sdk/models/operations/updatetrigger.go b/internal/sdk/models/operations/updatetrigger.go new file mode 100644 index 0000000..f024c2b --- /dev/null +++ b/internal/sdk/models/operations/updatetrigger.go @@ -0,0 +1,135 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/triggers" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type UpdateTriggerGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (u UpdateTriggerGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateTriggerGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateTriggerGlobals) GetAPIVersion() *string { + if u == nil { + return nil + } + return u.APIVersion +} + +func (u *UpdateTriggerGlobals) GetAPIRevision() *string { + if u == nil { + return nil + } + return u.APIRevision +} + +type UpdateTriggerRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Resource name of the trigger. + ID string `pathParam:"style=simple,explode=false,name=id"` + Body triggers.TriggerUpdate `request:"mediaType=application/json"` +} + +func (u UpdateTriggerRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateTriggerRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateTriggerRequest) GetAPIRevision() *string { + if u == nil { + return nil + } + return u.APIRevision +} + +func (u *UpdateTriggerRequest) GetAPIVersion() *string { + if u == nil { + return nil + } + return u.APIVersion +} + +func (u *UpdateTriggerRequest) GetID() string { + if u == nil { + return "" + } + return u.ID +} + +func (u *UpdateTriggerRequest) GetBody() triggers.TriggerUpdate { + if u == nil { + return triggers.TriggerUpdate{} + } + return u.Body +} + +type UpdateTriggerResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Trigger *triggers.Trigger +} + +func (u UpdateTriggerResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateTriggerResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateTriggerResponse) GetHTTPMeta() components.HTTPMetadata { + if u == nil { + return components.HTTPMetadata{} + } + return u.HTTPMeta +} + +func (u *UpdateTriggerResponse) GetTrigger() *triggers.Trigger { + if u == nil { + return nil + } + return u.Trigger +} diff --git a/internal/sdk/models/operations/updatewebhook.go b/internal/sdk/models/operations/updatewebhook.go new file mode 100644 index 0000000..4fe7772 --- /dev/null +++ b/internal/sdk/models/operations/updatewebhook.go @@ -0,0 +1,145 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type UpdateWebhookGlobals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` +} + +func (u UpdateWebhookGlobals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateWebhookGlobals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateWebhookGlobals) GetAPIVersion() *string { + if u == nil { + return nil + } + return u.APIVersion +} + +func (u *UpdateWebhookGlobals) GetAPIRevision() *string { + if u == nil { + return nil + } + return u.APIRevision +} + +type UpdateWebhookRequest struct { + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Required. The ID of the webhook to update. + ID string `pathParam:"style=simple,explode=false,name=id"` + // Optional. The list of fields to update. + UpdateMask *string `queryParam:"style=form,explode=true,name=update_mask"` + // Required. The webhook to update. + Body *webhooks.WebhookUpdate `request:"mediaType=application/json"` +} + +func (u UpdateWebhookRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateWebhookRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateWebhookRequest) GetAPIRevision() *string { + if u == nil { + return nil + } + return u.APIRevision +} + +func (u *UpdateWebhookRequest) GetAPIVersion() *string { + if u == nil { + return nil + } + return u.APIVersion +} + +func (u *UpdateWebhookRequest) GetID() string { + if u == nil { + return "" + } + return u.ID +} + +func (u *UpdateWebhookRequest) GetUpdateMask() *string { + if u == nil { + return nil + } + return u.UpdateMask +} + +func (u *UpdateWebhookRequest) GetBody() *webhooks.WebhookUpdate { + if u == nil { + return nil + } + return u.Body +} + +type UpdateWebhookResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Successful operation + Webhook *webhooks.Webhook +} + +func (u UpdateWebhookResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(u, "", false) +} + +func (u *UpdateWebhookResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &u, "", false, nil); err != nil { + return err + } + return nil +} + +func (u *UpdateWebhookResponse) GetHTTPMeta() components.HTTPMetadata { + if u == nil { + return components.HTTPMetadata{} + } + return u.HTTPMeta +} + +func (u *UpdateWebhookResponse) GetWebhook() *webhooks.Webhook { + if u == nil { + return nil + } + return u.Webhook +} diff --git a/internal/sdk/models/sdkerrors/cancelinteractionbyid.go b/internal/sdk/models/sdkerrors/cancelinteractionbyid.go new file mode 100644 index 0000000..2431a3c --- /dev/null +++ b/internal/sdk/models/sdkerrors/cancelinteractionbyid.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdkerrors + +import ( + "encoding/json" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" +) + +// CancelInteractionByIDServerError - Error cancelling interaction +type CancelInteractionByIDServerError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &CancelInteractionByIDServerError{} + +func (e *CancelInteractionByIDServerError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} + +// CancelInteractionByIDClientError - Error cancelling interaction +type CancelInteractionByIDClientError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &CancelInteractionByIDClientError{} + +func (e *CancelInteractionByIDClientError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} diff --git a/internal/sdk/models/sdkerrors/createinteraction.go b/internal/sdk/models/sdkerrors/createinteraction.go new file mode 100644 index 0000000..79d10b1 --- /dev/null +++ b/internal/sdk/models/sdkerrors/createinteraction.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdkerrors + +import ( + "encoding/json" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" +) + +// CreateInteractionServerError - Error creating interaction +type CreateInteractionServerError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &CreateInteractionServerError{} + +func (e *CreateInteractionServerError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} + +// CreateInteractionClientError - Error creating interaction +type CreateInteractionClientError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &CreateInteractionClientError{} + +func (e *CreateInteractionClientError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} diff --git a/internal/sdk/models/sdkerrors/deleteinteraction.go b/internal/sdk/models/sdkerrors/deleteinteraction.go new file mode 100644 index 0000000..9a808c6 --- /dev/null +++ b/internal/sdk/models/sdkerrors/deleteinteraction.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdkerrors + +import ( + "encoding/json" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" +) + +// DeleteInteractionServerError - Error deleting interaction +type DeleteInteractionServerError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &DeleteInteractionServerError{} + +func (e *DeleteInteractionServerError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} + +// DeleteInteractionClientError - Error deleting interaction +type DeleteInteractionClientError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &DeleteInteractionClientError{} + +func (e *DeleteInteractionClientError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} diff --git a/internal/sdk/models/sdkerrors/getinteractionbyid.go b/internal/sdk/models/sdkerrors/getinteractionbyid.go new file mode 100644 index 0000000..c8d11ed --- /dev/null +++ b/internal/sdk/models/sdkerrors/getinteractionbyid.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdkerrors + +import ( + "encoding/json" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" +) + +// GetInteractionByIDServerError - Error getting interaction +type GetInteractionByIDServerError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &GetInteractionByIDServerError{} + +func (e *GetInteractionByIDServerError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} + +// GetInteractionByIDClientError - Error getting interaction +type GetInteractionByIDClientError struct { + // Error message from an interaction. + ErrorInfo interactions.Error `json:"error"` + HTTPMeta components.HTTPMetadata `json:"-"` +} + +var _ error = &GetInteractionByIDClientError{} + +func (e *GetInteractionByIDClientError) Error() string { + data, _ := json.Marshal(e) + return string(data) +} diff --git a/internal/sdk/models/sdkerrors/sdkdefaulterror.go b/internal/sdk/models/sdkerrors/sdkdefaulterror.go new file mode 100644 index 0000000..158d827 --- /dev/null +++ b/internal/sdk/models/sdkerrors/sdkdefaulterror.go @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdkerrors + +import ( + "fmt" + "net/http" +) + +type SDKDefaultError struct { + Message string + StatusCode int + Body string + RawResponse *http.Response +} + +var _ error = &SDKDefaultError{} + +func NewSDKDefaultError(message string, statusCode int, body string, httpRes *http.Response) *SDKDefaultError { + return &SDKDefaultError{ + Message: message, + StatusCode: statusCode, + Body: body, + RawResponse: httpRes, + } +} + +func (e *SDKDefaultError) Error() string { + body := "" + if len(e.Body) > 0 { + body = fmt.Sprintf("\n%s", e.Body) + } + + return fmt.Sprintf("%s: Status %d%s", e.Message, e.StatusCode, body) +} diff --git a/internal/sdk/models/triggers/listtriggerexecutionsresponse.go b/internal/sdk/models/triggers/listtriggerexecutionsresponse.go new file mode 100644 index 0000000..3aba9a6 --- /dev/null +++ b/internal/sdk/models/triggers/listtriggerexecutionsresponse.go @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ListTriggerExecutionsResponse - Response message for TriggerService.ListTriggerExecutions. +type ListTriggerExecutionsResponse struct { + // A page token, received from a previous `ListTriggerExecutions` call. + // Provide this to retrieve the subsequent page. + NextPageToken *string `json:"next_page_token,omitzero"` + // The list of trigger executions. + TriggerExecutions []TriggerExecution `json:"trigger_executions,omitzero"` +} + +func (l ListTriggerExecutionsResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggerExecutionsResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggerExecutionsResponse) GetNextPageToken() *string { + if l == nil { + return nil + } + return l.NextPageToken +} + +func (l *ListTriggerExecutionsResponse) GetTriggerExecutions() []TriggerExecution { + if l == nil { + return nil + } + return l.TriggerExecutions +} diff --git a/internal/sdk/models/triggers/listtriggersresponse.go b/internal/sdk/models/triggers/listtriggersresponse.go new file mode 100644 index 0000000..2c54f07 --- /dev/null +++ b/internal/sdk/models/triggers/listtriggersresponse.go @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// ListTriggersResponse - Response message for TriggerService.ListTriggers. +type ListTriggersResponse struct { + // A page token, received from a previous `ListTriggers` call. + // Provide this to retrieve the subsequent page. + NextPageToken *string `json:"next_page_token,omitzero"` + // The list of triggers. + Triggers []Trigger `json:"triggers,omitzero"` +} + +func (l ListTriggersResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(l, "", false) +} + +func (l *ListTriggersResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &l, "", false, nil); err != nil { + return err + } + return nil +} + +func (l *ListTriggersResponse) GetNextPageToken() *string { + if l == nil { + return nil + } + return l.NextPageToken +} + +func (l *ListTriggersResponse) GetTriggers() []Trigger { + if l == nil { + return nil + } + return l.Triggers +} diff --git a/internal/sdk/models/triggers/trigger.go b/internal/sdk/models/triggers/trigger.go new file mode 100644 index 0000000..a7eb213 --- /dev/null +++ b/internal/sdk/models/triggers/trigger.go @@ -0,0 +1,219 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// TriggerStatus - Output only. The current status of the trigger. +type TriggerStatus string + +const ( + TriggerStatusActive TriggerStatus = "active" + TriggerStatusPaused TriggerStatus = "paused" + TriggerStatusError TriggerStatus = "error" +) + +func (e TriggerStatus) ToPointer() *TriggerStatus { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *TriggerStatus) IsExact() bool { + if e != nil { + switch *e { + case "active", "paused", "error": + return true + } + } + return false +} + +// Trigger - A trigger configuration that is scheduled to run an agent. +type Trigger struct { + // Output only. The number of consecutive failures that have occurred + // since the last successful execution. + ConsecutiveFailureCount *int `json:"consecutive_failure_count,omitzero"` + // Output only. The time when the trigger was created. + CreateTime *time.Time `json:"create_time,omitzero"` + // Optional. The display name of the trigger. + DisplayName *string `json:"display_name,omitzero"` + // Optional. The environment ID for the trigger execution. + EnvironmentID *string `json:"environment_id,omitzero"` + // Optional. The execution timeout for the triggered interaction. + ExecutionTimeoutSeconds *int `json:"execution_timeout_seconds,omitzero"` + // Required. Output only. Identifier. The ID of the trigger. + ID string `json:"id"` + // The Interaction resource. + Interaction interactions.Interaction `json:"interaction"` + // Output only. The time when the trigger was last paused. + LastPauseTime *time.Time `json:"last_pause_time,omitzero"` + // Output only. The time when the trigger was last resumed. + LastResumeTime *time.Time `json:"last_resume_time,omitzero"` + // Output only. The time when the trigger was last run. + LastRunTime *time.Time `json:"last_run_time,omitzero"` + // Optional. The maximum number of consecutive failures allowed before + // the trigger is automatically paused (status becomes ERROR). + MaxConsecutiveFailures *int `json:"max_consecutive_failures,omitzero"` + // Output only. The time when the trigger is scheduled to run next. + NextRunTime *time.Time `json:"next_run_time,omitzero"` + // Output only. The ID of the last interaction created by this trigger. + PreviousInteractionID *string `json:"previous_interaction_id,omitzero"` + // Required. The cron schedule on which the trigger should run. + // Standard cron format. + Schedule string `json:"schedule"` + // Output only. The current status of the trigger. + Status *TriggerStatus `json:"status,omitzero"` + // Required. Time zone in which the schedule should be interpreted. + TimeZone string `json:"time_zone"` + // Output only. The time when the trigger was last updated. + UpdateTime *time.Time `json:"update_time,omitzero"` +} + +func (t Trigger) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *Trigger) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *Trigger) GetConsecutiveFailureCount() *int { + if t == nil { + return nil + } + return t.ConsecutiveFailureCount +} + +func (t *Trigger) GetCreateTime() *time.Time { + if t == nil { + return nil + } + return t.CreateTime +} + +func (t *Trigger) GetDisplayName() *string { + if t == nil { + return nil + } + return t.DisplayName +} + +func (t *Trigger) GetEnvironmentID() *string { + if t == nil { + return nil + } + return t.EnvironmentID +} + +func (t *Trigger) GetExecutionTimeoutSeconds() *int { + if t == nil { + return nil + } + return t.ExecutionTimeoutSeconds +} + +func (t *Trigger) GetID() string { + if t == nil { + return "" + } + return t.ID +} + +func (t *Trigger) GetInteraction() interactions.Interaction { + if t == nil { + return interactions.Interaction{} + } + return t.Interaction +} + +func (t *Trigger) GetLastPauseTime() *time.Time { + if t == nil { + return nil + } + return t.LastPauseTime +} + +func (t *Trigger) GetLastResumeTime() *time.Time { + if t == nil { + return nil + } + return t.LastResumeTime +} + +func (t *Trigger) GetLastRunTime() *time.Time { + if t == nil { + return nil + } + return t.LastRunTime +} + +func (t *Trigger) GetMaxConsecutiveFailures() *int { + if t == nil { + return nil + } + return t.MaxConsecutiveFailures +} + +func (t *Trigger) GetNextRunTime() *time.Time { + if t == nil { + return nil + } + return t.NextRunTime +} + +func (t *Trigger) GetPreviousInteractionID() *string { + if t == nil { + return nil + } + return t.PreviousInteractionID +} + +func (t *Trigger) GetSchedule() string { + if t == nil { + return "" + } + return t.Schedule +} + +func (t *Trigger) GetStatus() *TriggerStatus { + if t == nil { + return nil + } + return t.Status +} + +func (t *Trigger) GetTimeZone() string { + if t == nil { + return "" + } + return t.TimeZone +} + +func (t *Trigger) GetUpdateTime() *time.Time { + if t == nil { + return nil + } + return t.UpdateTime +} diff --git a/internal/sdk/models/triggers/triggerexecution.go b/internal/sdk/models/triggers/triggerexecution.go new file mode 100644 index 0000000..40d8a31 --- /dev/null +++ b/internal/sdk/models/triggers/triggerexecution.go @@ -0,0 +1,145 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// TriggerExecutionStatus - Output only. The status of the execution. +type TriggerExecutionStatus string + +const ( + TriggerExecutionStatusInProgress TriggerExecutionStatus = "in_progress" + TriggerExecutionStatusCompleted TriggerExecutionStatus = "completed" + TriggerExecutionStatusFailed TriggerExecutionStatus = "failed" + TriggerExecutionStatusSkipped TriggerExecutionStatus = "skipped" + TriggerExecutionStatusTimedOut TriggerExecutionStatus = "timed_out" +) + +func (e TriggerExecutionStatus) ToPointer() *TriggerExecutionStatus { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *TriggerExecutionStatus) IsExact() bool { + if e != nil { + switch *e { + case "in_progress", "completed", "failed", "skipped", "timed_out": + return true + } + } + return false +} + +// TriggerExecution - An execution instance of a trigger. +type TriggerExecution struct { + // Output only. The time when the execution finished. + EndTime *time.Time `json:"end_time,omitzero"` + // Output only. The environment ID used for the execution. + EnvironmentID *string `json:"environment_id,omitzero"` + // Output only. The error message if the execution failed. + Error *string `json:"error,omitzero"` + // Required. Output only. Identifier. The ID of the trigger execution. + ID string `json:"id"` + // Output only. The ID of the interaction created by this execution, if any. + InteractionID *string `json:"interaction_id,omitzero"` + // Output only. The time when the execution was scheduled to run. + ScheduledTime *time.Time `json:"scheduled_time,omitzero"` + // Output only. The time when the execution started. + StartTime *time.Time `json:"start_time,omitzero"` + // Output only. The status of the execution. + Status *TriggerExecutionStatus `json:"status,omitzero"` + // Required. Output only. Identifier. The ID of the trigger that created this execution. + TriggerID string `json:"trigger_id"` +} + +func (t TriggerExecution) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(t, "", false) +} + +func (t *TriggerExecution) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &t, "", false, nil); err != nil { + return err + } + return nil +} + +func (t *TriggerExecution) GetEndTime() *time.Time { + if t == nil { + return nil + } + return t.EndTime +} + +func (t *TriggerExecution) GetEnvironmentID() *string { + if t == nil { + return nil + } + return t.EnvironmentID +} + +func (t *TriggerExecution) GetError() *string { + if t == nil { + return nil + } + return t.Error +} + +func (t *TriggerExecution) GetID() string { + if t == nil { + return "" + } + return t.ID +} + +func (t *TriggerExecution) GetInteractionID() *string { + if t == nil { + return nil + } + return t.InteractionID +} + +func (t *TriggerExecution) GetScheduledTime() *time.Time { + if t == nil { + return nil + } + return t.ScheduledTime +} + +func (t *TriggerExecution) GetStartTime() *time.Time { + if t == nil { + return nil + } + return t.StartTime +} + +func (t *TriggerExecution) GetStatus() *TriggerExecutionStatus { + if t == nil { + return nil + } + return t.Status +} + +func (t *TriggerExecution) GetTriggerID() string { + if t == nil { + return "" + } + return t.TriggerID +} diff --git a/internal/sdk/models/triggers/triggerupdate.go b/internal/sdk/models/triggers/triggerupdate.go new file mode 100644 index 0000000..6c77bc8 --- /dev/null +++ b/internal/sdk/models/triggers/triggerupdate.go @@ -0,0 +1,74 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package triggers + +import ( + "encoding/json" + "fmt" +) + +// TriggerUpdateStatus - Optional. The status of the trigger. +type TriggerUpdateStatus string + +const ( + TriggerUpdateStatusActive TriggerUpdateStatus = "active" + TriggerUpdateStatusPaused TriggerUpdateStatus = "paused" + TriggerUpdateStatusError TriggerUpdateStatus = "error" +) + +func (e TriggerUpdateStatus) ToPointer() *TriggerUpdateStatus { + return &e +} +func (e *TriggerUpdateStatus) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "active": + fallthrough + case "paused": + fallthrough + case "error": + *e = TriggerUpdateStatus(v) + return nil + default: + return fmt.Errorf("invalid value for TriggerUpdateStatus: %v", v) + } +} + +// TriggerUpdate - Represents the fields of a Trigger that can be updated. +type TriggerUpdate struct { + // Optional. The display name of the trigger. + DisplayName *string `json:"display_name,omitzero"` + // Optional. The status of the trigger. + Status *TriggerUpdateStatus `json:"status,omitzero"` +} + +func (t *TriggerUpdate) GetDisplayName() *string { + if t == nil { + return nil + } + return t.DisplayName +} + +func (t *TriggerUpdate) GetStatus() *TriggerUpdateStatus { + if t == nil { + return nil + } + return t.Status +} diff --git a/src/lib/yaml.ts b/internal/sdk/models/webhooks/pingwebhookrequest.go similarity index 69% rename from src/lib/yaml.ts rename to internal/sdk/models/webhooks/pingwebhookrequest.go index 3423999..15ec4a4 100644 --- a/src/lib/yaml.ts +++ b/internal/sdk/models/webhooks/pingwebhookrequest.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import yaml from "js-yaml"; +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. -export function parseYaml(content: string): unknown { - return yaml.load(content); +package webhooks + +// PingWebhookRequest - Request message for WebhookService.PingWebhook. +type PingWebhookRequest struct { } diff --git a/internal/sdk/models/webhooks/rotatesigningsecretrequest.go b/internal/sdk/models/webhooks/rotatesigningsecretrequest.go new file mode 100644 index 0000000..2c4d689 --- /dev/null +++ b/internal/sdk/models/webhooks/rotatesigningsecretrequest.go @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "encoding/json" + "fmt" +) + +// RevocationBehavior - Optional. The revocation behavior for previous signing secrets. +type RevocationBehavior string + +const ( + RevocationBehaviorRevokePreviousSecretsAfterH24 RevocationBehavior = "revoke_previous_secrets_after_h24" + RevocationBehaviorRevokePreviousSecretsImmediately RevocationBehavior = "revoke_previous_secrets_immediately" +) + +func (e RevocationBehavior) ToPointer() *RevocationBehavior { + return &e +} +func (e *RevocationBehavior) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "revoke_previous_secrets_after_h24": + fallthrough + case "revoke_previous_secrets_immediately": + *e = RevocationBehavior(v) + return nil + default: + return fmt.Errorf("invalid value for RevocationBehavior: %v", v) + } +} + +// RotateSigningSecretRequest - Request message for WebhookService.RotateSigningSecret. +type RotateSigningSecretRequest struct { + // Optional. The revocation behavior for previous signing secrets. + RevocationBehavior *RevocationBehavior `json:"revocation_behavior,omitzero"` +} + +func (r *RotateSigningSecretRequest) GetRevocationBehavior() *RevocationBehavior { + if r == nil { + return nil + } + return r.RevocationBehavior +} diff --git a/internal/sdk/models/webhooks/signingsecret.go b/internal/sdk/models/webhooks/signingsecret.go new file mode 100644 index 0000000..d927282 --- /dev/null +++ b/internal/sdk/models/webhooks/signingsecret.go @@ -0,0 +1,56 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// SigningSecret - Represents a signing secret used to verify webhook payloads. +type SigningSecret struct { + // Output only. The expiration date of the signing secret. + ExpireTime *time.Time `json:"expire_time,omitzero"` + // Output only. The truncated version of the signing secret. + TruncatedSecret *string `json:"truncated_secret,omitzero"` +} + +func (s SigningSecret) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(s, "", false) +} + +func (s *SigningSecret) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &s, "", false, nil); err != nil { + return err + } + return nil +} + +func (s *SigningSecret) GetExpireTime() *time.Time { + if s == nil { + return nil + } + return s.ExpireTime +} + +func (s *SigningSecret) GetTruncatedSecret() *string { + if s == nil { + return nil + } + return s.TruncatedSecret +} diff --git a/internal/sdk/models/webhooks/webhook.go b/internal/sdk/models/webhooks/webhook.go new file mode 100644 index 0000000..e87e21a --- /dev/null +++ b/internal/sdk/models/webhooks/webhook.go @@ -0,0 +1,224 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// WebhookState - Output only. The state of the webhook. +type WebhookState string + +const ( + WebhookStateEnabled WebhookState = "enabled" + WebhookStateDisabled WebhookState = "disabled" + WebhookStateDisabledDueToFailedDeliveries WebhookState = "disabled_due_to_failed_deliveries" +) + +func (e WebhookState) ToPointer() *WebhookState { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *WebhookState) IsExact() bool { + if e != nil { + switch *e { + case "enabled", "disabled", "disabled_due_to_failed_deliveries": + return true + } + } + return false +} + +type WebhookSubscribedEvent string + +const ( + // WebhookSubscribedEventBatchSucceeded Batch processing finished successfully. + WebhookSubscribedEventBatchSucceeded WebhookSubscribedEvent = "batch.succeeded" + // WebhookSubscribedEventBatchExpired Batch has not been processed within the 48h timeframe. + WebhookSubscribedEventBatchExpired WebhookSubscribedEvent = "batch.expired" + // WebhookSubscribedEventBatchFailed Batch job failed. + WebhookSubscribedEventBatchFailed WebhookSubscribedEvent = "batch.failed" + // WebhookSubscribedEventInteractionRequiresAction Interaction requires action (e.g., function calling). + WebhookSubscribedEventInteractionRequiresAction WebhookSubscribedEvent = "interaction.requires_action" + // WebhookSubscribedEventInteractionCompleted Interaction completed successfully. + WebhookSubscribedEventInteractionCompleted WebhookSubscribedEvent = "interaction.completed" + // WebhookSubscribedEventInteractionFailed Interaction failed. + WebhookSubscribedEventInteractionFailed WebhookSubscribedEvent = "interaction.failed" + // WebhookSubscribedEventVideoGenerated Video generation completed. + WebhookSubscribedEventVideoGenerated WebhookSubscribedEvent = "video.generated" +) + +func (e WebhookSubscribedEvent) ToPointer() *WebhookSubscribedEvent { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *WebhookSubscribedEvent) IsExact() bool { + if e != nil { + switch *e { + case "batch.succeeded", "batch.expired", "batch.failed", "interaction.requires_action", "interaction.completed", "interaction.failed", "video.generated": + return true + } + } + return false +} + +// A Webhook resource. +type Webhook struct { + // Output only. The timestamp when the webhook was created. + CreateTime *time.Time `json:"create_time,omitzero"` + // Output only. The ID of the webhook. + ID *string `json:"id,omitzero"` + // Optional. The user-provided name of the webhook. + Name *string `json:"name,omitzero"` + // Output only. The new signing secret for the webhook. Only populated on create. + NewSigningSecret *string `json:"new_signing_secret,omitzero"` + // Output only. The signing secrets associated with this webhook. + SigningSecrets []SigningSecret `json:"signing_secrets,omitzero"` + // Output only. The state of the webhook. + State *WebhookState `json:"state,omitzero"` + // Required. The events that the webhook is subscribed to. + // Available events: + // - batch.succeeded + // - batch.expired + // - batch.failed + // - interaction.requires_action + // - interaction.completed + // - interaction.failed + // - video.generated + SubscribedEvents []WebhookSubscribedEvent `json:"subscribed_events"` + // Output only. The timestamp when the webhook was last updated. + UpdateTime *time.Time `json:"update_time,omitzero"` + // Required. The URI to which webhook events will be sent. + URI string `json:"uri"` +} + +func (w Webhook) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(w, "", false) +} + +func (w *Webhook) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { + return err + } + return nil +} + +func (w *Webhook) GetCreateTime() *time.Time { + if w == nil { + return nil + } + return w.CreateTime +} + +func (w *Webhook) GetID() *string { + if w == nil { + return nil + } + return w.ID +} + +func (w *Webhook) GetName() *string { + if w == nil { + return nil + } + return w.Name +} + +func (w *Webhook) GetNewSigningSecret() *string { + if w == nil { + return nil + } + return w.NewSigningSecret +} + +func (w *Webhook) GetSigningSecrets() []SigningSecret { + if w == nil { + return nil + } + return w.SigningSecrets +} + +func (w *Webhook) GetState() *WebhookState { + if w == nil { + return nil + } + return w.State +} + +func (w *Webhook) GetSubscribedEvents() []WebhookSubscribedEvent { + if w == nil { + return []WebhookSubscribedEvent{} + } + return w.SubscribedEvents +} + +func (w *Webhook) GetUpdateTime() *time.Time { + if w == nil { + return nil + } + return w.UpdateTime +} + +func (w *Webhook) GetURI() string { + if w == nil { + return "" + } + return w.URI +} + +// WebhookInput - A Webhook resource. +type WebhookInput struct { + // Optional. The user-provided name of the webhook. + Name *string `json:"name,omitzero"` + // Required. The events that the webhook is subscribed to. + // Available events: + // - batch.succeeded + // - batch.expired + // - batch.failed + // - interaction.requires_action + // - interaction.completed + // - interaction.failed + // - video.generated + SubscribedEvents []WebhookSubscribedEvent `json:"subscribed_events"` + // Required. The URI to which webhook events will be sent. + URI string `json:"uri"` +} + +func (w *WebhookInput) GetName() *string { + if w == nil { + return nil + } + return w.Name +} + +func (w *WebhookInput) GetSubscribedEvents() []WebhookSubscribedEvent { + if w == nil { + return []WebhookSubscribedEvent{} + } + return w.SubscribedEvents +} + +func (w *WebhookInput) GetURI() string { + if w == nil { + return "" + } + return w.URI +} diff --git a/internal/sdk/models/webhooks/webhooklistresponse.go b/internal/sdk/models/webhooks/webhooklistresponse.go new file mode 100644 index 0000000..2e079b9 --- /dev/null +++ b/internal/sdk/models/webhooks/webhooklistresponse.go @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// WebhookListResponse - Response message for WebhookService.ListWebhooks. +type WebhookListResponse struct { + // A token, which can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + NextPageToken *string `json:"next_page_token,omitzero"` + // The webhooks. + Webhooks []Webhook `json:"webhooks,omitzero"` +} + +func (w WebhookListResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(w, "", false) +} + +func (w *WebhookListResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { + return err + } + return nil +} + +func (w *WebhookListResponse) GetNextPageToken() *string { + if w == nil { + return nil + } + return w.NextPageToken +} + +func (w *WebhookListResponse) GetWebhooks() []Webhook { + if w == nil { + return nil + } + return w.Webhooks +} diff --git a/src/types.ts b/internal/sdk/models/webhooks/webhookpingresponse.go similarity index 68% rename from src/types.ts rename to internal/sdk/models/webhooks/webhookpingresponse.go index f9803d3..76fae70 100644 --- a/src/types.ts +++ b/internal/sdk/models/webhooks/webhookpingresponse.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -12,4 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -// TODO: Implement — see tasks/task_3.md +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +// WebhookPingResponse - Response message for WebhookService.PingWebhook. +type WebhookPingResponse struct { +} diff --git a/internal/sdk/models/webhooks/webhookrotatesigningsecretresponse.go b/internal/sdk/models/webhooks/webhookrotatesigningsecretresponse.go new file mode 100644 index 0000000..2af3091 --- /dev/null +++ b/internal/sdk/models/webhooks/webhookrotatesigningsecretresponse.go @@ -0,0 +1,30 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +// WebhookRotateSigningSecretResponse - Response message for WebhookService.RotateSigningSecret. +type WebhookRotateSigningSecretResponse struct { + // Output only. The newly generated signing secret. + Secret *string `json:"secret,omitzero"` +} + +func (w *WebhookRotateSigningSecretResponse) GetSecret() *string { + if w == nil { + return nil + } + return w.Secret +} diff --git a/internal/sdk/models/webhooks/webhookupdate.go b/internal/sdk/models/webhooks/webhookupdate.go new file mode 100644 index 0000000..b179701 --- /dev/null +++ b/internal/sdk/models/webhooks/webhookupdate.go @@ -0,0 +1,146 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package webhooks + +import ( + "encoding/json" + "fmt" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +// WebhookUpdateState - Optional. The state of the webhook. +type WebhookUpdateState string + +const ( + WebhookUpdateStateEnabled WebhookUpdateState = "enabled" + WebhookUpdateStateDisabled WebhookUpdateState = "disabled" + WebhookUpdateStateDisabledDueToFailedDeliveries WebhookUpdateState = "disabled_due_to_failed_deliveries" +) + +func (e WebhookUpdateState) ToPointer() *WebhookUpdateState { + return &e +} +func (e *WebhookUpdateState) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "enabled": + fallthrough + case "disabled": + fallthrough + case "disabled_due_to_failed_deliveries": + *e = WebhookUpdateState(v) + return nil + default: + return fmt.Errorf("invalid value for WebhookUpdateState: %v", v) + } +} + +type WebhookUpdateSubscribedEvent string + +const ( + // WebhookUpdateSubscribedEventBatchSucceeded Batch processing finished successfully. + WebhookUpdateSubscribedEventBatchSucceeded WebhookUpdateSubscribedEvent = "batch.succeeded" + // WebhookUpdateSubscribedEventBatchExpired Batch has not been processed within the 48h timeframe. + WebhookUpdateSubscribedEventBatchExpired WebhookUpdateSubscribedEvent = "batch.expired" + // WebhookUpdateSubscribedEventBatchFailed Batch job failed. + WebhookUpdateSubscribedEventBatchFailed WebhookUpdateSubscribedEvent = "batch.failed" + // WebhookUpdateSubscribedEventInteractionRequiresAction Interaction requires action (e.g., function calling). + WebhookUpdateSubscribedEventInteractionRequiresAction WebhookUpdateSubscribedEvent = "interaction.requires_action" + // WebhookUpdateSubscribedEventInteractionCompleted Interaction completed successfully. + WebhookUpdateSubscribedEventInteractionCompleted WebhookUpdateSubscribedEvent = "interaction.completed" + // WebhookUpdateSubscribedEventInteractionFailed Interaction failed. + WebhookUpdateSubscribedEventInteractionFailed WebhookUpdateSubscribedEvent = "interaction.failed" + // WebhookUpdateSubscribedEventVideoGenerated Video generation completed. + WebhookUpdateSubscribedEventVideoGenerated WebhookUpdateSubscribedEvent = "video.generated" +) + +func (e WebhookUpdateSubscribedEvent) ToPointer() *WebhookUpdateSubscribedEvent { + return &e +} + +// IsExact returns true if the value matches a known enum value, false otherwise. +func (e *WebhookUpdateSubscribedEvent) IsExact() bool { + if e != nil { + switch *e { + case "batch.succeeded", "batch.expired", "batch.failed", "interaction.requires_action", "interaction.completed", "interaction.failed", "video.generated": + return true + } + } + return false +} + +type WebhookUpdate struct { + // Optional. The user-provided name of the webhook. + Name *string `json:"name,omitzero"` + // Optional. The state of the webhook. + State *WebhookUpdateState `json:"state,omitzero"` + // Optional. The events that the webhook is subscribed to. + // Available events: + // - batch.succeeded + // - batch.expired + // - batch.failed + // - interaction.requires_action + // - interaction.completed + // - interaction.failed + // - video.generated + SubscribedEvents []WebhookUpdateSubscribedEvent `json:"subscribed_events,omitzero"` + // Optional. The URI to which webhook events will be sent. + URI *string `json:"uri,omitzero"` +} + +func (w WebhookUpdate) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(w, "", false) +} + +func (w *WebhookUpdate) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { + return err + } + return nil +} + +func (w *WebhookUpdate) GetName() *string { + if w == nil { + return nil + } + return w.Name +} + +func (w *WebhookUpdate) GetState() *WebhookUpdateState { + if w == nil { + return nil + } + return w.State +} + +func (w *WebhookUpdate) GetSubscribedEvents() []WebhookUpdateSubscribedEvent { + if w == nil { + return nil + } + return w.SubscribedEvents +} + +func (w *WebhookUpdate) GetURI() *string { + if w == nil { + return nil + } + return w.URI +} diff --git a/internal/sdk/optionalnullable/optionalnullable.go b/internal/sdk/optionalnullable/optionalnullable.go new file mode 100644 index 0000000..800359b --- /dev/null +++ b/internal/sdk/optionalnullable/optionalnullable.go @@ -0,0 +1,247 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package optionalnullable + +import ( + "bytes" + "encoding/json" + "reflect" +) + +// OptionalNullable represents a field that can distinguish between three states: +// 1. Set to a value: field is present with a non-nil value +// 2. Set to nil: field is present but explicitly set to null in JSON +// 3. Unset: field is omitted from JSON entirely +// +// This type is designed to work with JSON marshaling/unmarshaling and supports +// the `omitempty` struct tag to properly omit unset fields from JSON output. +// +// Usage: +// +// type User struct { +// Name OptionalNullable[string] `json:"name,omitempty"` +// Age OptionalNullable[int] `json:"age,omitempty"` +// Tags OptionalNullable[[]string] `json:"tags,omitempty"` +// } +// +// // Set to value +// name := "John" +// user.Name = From(&name) +// +// // Set to nil (will appear as "name": null in JSON) +// user.Name = From[string](nil) +// +// // Leave unset (will be omitted from JSON with omitempty) +// user := User{} +// +// WARNING: Do NOT use *OptionalNullable[T] as a field type. Always use OptionalNullable[T] directly. +// Using *OptionalNullable[T] will break the omitempty behavior and JSON marshaling. +// +// The type is implemented as a map[bool]*T where: +// - nil map represents unset state +// - Map with true key represents set state (value may be nil) +type OptionalNullable[T any] map[bool]*T + +// From creates a new OptionalNullable with the given value. +// Pass nil to create a OptionalNullable that is set to null. +// Pass a pointer to a value to create a OptionalNullable with that value. +// +// Examples: +// +// hello := "hello" +// From(&hello) // set to "hello" +// From[string](nil) // set to null +func From[T any](value *T) OptionalNullable[T] { + return map[bool]*T{ + true: value, + } +} + +// IsNull returns true if the OptionalNullable is explicitly set to nil. +// Returns false if the OptionalNullable is unset or has a value. +// +// Note: This differs from traditional null checks because unset fields +// return false, not true. Use IsSet() to check if a field was provided. +func (n OptionalNullable[T]) IsNull() bool { + v, ok := n[true] + return ok && v == nil +} + +// IsSet returns true if the OptionalNullable has been explicitly set (to either a value or nil). +// Returns false if the OptionalNullable is unset (omitted from JSON). +// +// This is the key method for distinguishing between: +// - Set to nil: IsSet() = true, IsNull() = true +// - Unset: IsSet() = false, IsNull() = false +func (n OptionalNullable[T]) IsSet() bool { + _, ok := n[true] + return ok +} + +// Get returns the internal pointer and whether the field was set. +// +// Return values: +// - (ptr, true): field was set (ptr may be nil if set to null) +// - (nil, false): field was unset/omitted +// +// This method provides direct access to the internal pointer representation. +func (n OptionalNullable[T]) Get() (*T, bool) { + v, ok := n[true] + return v, ok +} + +// GetOrZero returns the value and whether it was set. +// +// Return values: +// - (value, true): field was set to a non-nil value +// - (zero, true): field was explicitly set to nil +// - (zero, false): field was unset/omitted +// +// Examples: +// +// val, ok := nullable.GetOrZero() +// if !ok { +// // Field was unset/omitted +// } else if nullable.IsNull() { +// // Field was explicitly set to null +// } else { +// // Field has a value: val +// } +func (n OptionalNullable[T]) GetOrZero() (T, bool) { + var zero T + + if v, ok := n[true]; ok { + if v == nil { + return zero, true + } + return *v, true + } + return zero, false +} + +// GetUntyped returns the value as interface{} and whether it was set. +// This is useful for reflection-based code that needs to work with the value +// without knowing the specific type T. +// +// Return values: +// - (value, true): field was set to a non-nil value +// - (nil, true): field was explicitly set to nil +// - (nil, false): field was unset/omitted +func (n OptionalNullable[T]) GetUntyped() (interface{}, bool) { + if v, ok := n[true]; ok { + if v == nil { + return nil, true + } + return *v, true + } + return nil, false +} + +// Set sets the OptionalNullable to the given value pointer. +// Pass nil to set the field to null. +// Pass a pointer to a value to set the field to that value. +// +// Examples: +// +// nullable.Set(ptrFrom("hello")) // set to "hello" +// nullable.Set(nil) // set to null +func (n *OptionalNullable[T]) Set(value *T) { + *n = map[bool]*T{ + true: value, + } +} + +// Unset removes the value, making the field unset/omitted. +// After calling Unset(), IsSet() will return false and the field +// will be omitted from JSON output when using omitempty. +func (n *OptionalNullable[T]) Unset() { + *n = map[bool]*T{} +} + +// MarshalJSON implements json.Marshaler. +// +// Behavior: +// - Unset fields: omitted from JSON when struct field has omitempty tag +// - Null fields: serialized as "null" +// - Value fields: serialized as the actual value +// +// The omitempty behavior works because an empty map is considered +// a zero value by Go's JSON package. +func (n OptionalNullable[T]) MarshalJSON() ([]byte, error) { + if n.IsNull() { + return []byte("null"), nil + } + + return json.Marshal(n[true]) +} + +// UnmarshalJSON implements json.Unmarshaler. +// +// Behavior: +// - "null" in JSON: sets the field to null (IsSet=true, IsNull=true) +// - Any other value: sets the field to that value (IsSet=true, IsNull=false) +// - Missing from JSON: field remains unset (IsSet=false, IsNull=false) +func (n *OptionalNullable[T]) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("null")) { + n.Set(nil) + return nil + } + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Set(&v) + return nil +} + +// NullableInterface defines the interface that all OptionalNullable[T] types implement. +// This interface provides untyped access to optional nullable values for reflection-based code. +type OptionalNullableInterface interface { + GetUntyped() (interface{}, bool) +} + +// AsOptionalNullable attempts to convert a reflect.Value to a OptionalNullableInterface. +// This is a helper function for reflection-based code that needs to check +// if a value implements the optional nullable interface pattern. +// +// Returns: +// - (nullable, true): if the value implements OptionalNullableInterface +// - (nil, false): if the value does not implement OptionalNullableInterface +// +// Example usage: +// +// if nullable, ok := AsOptionalNullable(reflectValue); ok { +// if value, isSet := nullable.GetUntyped(); isSet { +// // Handle the nullable value +// } +// } +func AsOptionalNullable(v reflect.Value) (OptionalNullableInterface, bool) { + // Check if the value can be converted to an interface first + if !v.CanInterface() { + return nil, false + } + + // Check if the underlying value is a nil map (unset nullable) + if v.Kind() == reflect.Map && v.IsNil() { + return nil, false + } + + if nullable, ok := v.Interface().(OptionalNullableInterface); ok { + return nullable, true + } + return nil, false +} diff --git a/internal/sdk/optionalnullable/optionalnullable_test.go b/internal/sdk/optionalnullable/optionalnullable_test.go new file mode 100644 index 0000000..fd549c2 --- /dev/null +++ b/internal/sdk/optionalnullable/optionalnullable_test.go @@ -0,0 +1,1927 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package optionalnullable + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" +) + +func msgSuffix(msgAndArgs ...any) string { + if len(msgAndArgs) == 0 { + return "" + } + if format, ok := msgAndArgs[0].(string); ok && len(msgAndArgs) > 1 { + return ": " + fmt.Sprintf(format, msgAndArgs[1:]...) + } + return ": " + fmt.Sprint(msgAndArgs...) +} + +func isNil(v any) bool { + if v == nil { + return true + } + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return rv.IsNil() + } + return false +} + +func containsElement(container, elem any) bool { + cv := reflect.ValueOf(container) + switch cv.Kind() { + case reflect.String: + return strings.Contains(cv.String(), reflect.ValueOf(elem).String()) + case reflect.Map: + for _, k := range cv.MapKeys() { + if reflect.DeepEqual(k.Interface(), elem) { + return true + } + } + return false + case reflect.Slice, reflect.Array: + for i := 0; i < cv.Len(); i++ { + if reflect.DeepEqual(cv.Index(i).Interface(), elem) { + return true + } + } + return false + } + return false +} + +func assertEqual(t *testing.T, want, got any, msgAndArgs ...any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Errorf("expected %#v, got %#v%s", want, got, msgSuffix(msgAndArgs...)) + } +} + +func assertTrue(t *testing.T, cond bool, msgAndArgs ...any) { + t.Helper() + if !cond { + t.Errorf("expected true%s", msgSuffix(msgAndArgs...)) + } +} + +func assertFalse(t *testing.T, cond bool, msgAndArgs ...any) { + t.Helper() + if cond { + t.Errorf("expected false%s", msgSuffix(msgAndArgs...)) + } +} + +func assertNil(t *testing.T, v any, msgAndArgs ...any) { + t.Helper() + if !isNil(v) { + t.Errorf("expected nil, got %#v%s", v, msgSuffix(msgAndArgs...)) + } +} + +func assertNotNil(t *testing.T, v any, msgAndArgs ...any) { + t.Helper() + if isNil(v) { + t.Errorf("expected non-nil value%s", msgSuffix(msgAndArgs...)) + } +} + +func assertError(t *testing.T, err error, msgAndArgs ...any) { + t.Helper() + if err == nil { + t.Errorf("expected an error%s", msgSuffix(msgAndArgs...)) + } +} + +func assertContains(t *testing.T, container, elem any, msgAndArgs ...any) { + t.Helper() + if !containsElement(container, elem) { + t.Errorf("%#v does not contain %#v%s", container, elem, msgSuffix(msgAndArgs...)) + } +} + +func assertNotContains(t *testing.T, container, elem any, msgAndArgs ...any) { + t.Helper() + if containsElement(container, elem) { + t.Errorf("%#v should not contain %#v%s", container, elem, msgSuffix(msgAndArgs...)) + } +} + +func mustNoError(t *testing.T, err error, msgAndArgs ...any) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v%s", err, msgSuffix(msgAndArgs...)) + } +} + +// Test helper function to create pointers from values +func ptrFrom[T any](value T) *T { + return &value +} + +// Test helper types for comprehensive testing +type TestStruct struct { + Name string `json:"name"` + Age int `json:"age"` +} + +type TestContainer struct { + StringField OptionalNullable[string] `json:"string_field,omitempty"` + IntField OptionalNullable[int] `json:"int_field,omitempty"` + SliceField OptionalNullable[[]string] `json:"slice_field,omitempty"` + StructField OptionalNullable[TestStruct] `json:"struct_field,omitempty"` +} + +// TestNewNullable tests the From constructor +func TestNewNullable(t *testing.T) { + t.Parallel() + t.Run("with string value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "test", got) + }) + + t.Run("with nil pointer", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "", got) // zero value for string + }) + + t.Run("with int value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom(42)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 42, got) + }) + + t.Run("with slice value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom([]string{"a", "b", "c"})) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b", "c"}, got) + }) + + t.Run("with empty slice", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom([]string{})) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{}, got) + }) + + t.Run("with struct value", func(t *testing.T) { + t.Parallel() + val := TestStruct{Name: "John", Age: 30} + nullable := From(&val) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + v, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, val, v) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, TestStruct{Name: "John", Age: 30}, got) + }) +} + +// TestNewNullableUnset tests the NewNullableUnset constructor +func TestNewNullableUnset(t *testing.T) { + t.Parallel() + t.Run("string type", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // Unset is not null + + got, ok := nullable.GetOrZero() + assertFalse(t, ok) + assertEqual(t, "", got) // zero value for string + }) + + t.Run("int type", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[int] + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // Unset is not null + + got, ok := nullable.GetOrZero() + assertFalse(t, ok) + assertEqual(t, 0, got) // zero value for int + }) + + t.Run("slice type", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[[]string] + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // Unset is not null + + got, ok := nullable.GetOrZero() + assertFalse(t, ok) + assertNil(t, got) // zero value for slice is nil + }) +} + +// TestIsNull tests the IsNull method +func TestIsNull(t *testing.T) { + t.Parallel() + t.Run("with value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + assertFalse(t, nullable.IsNull()) + }) + + t.Run("with nil pointer", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + assertTrue(t, nullable.IsNull()) + }) + + t.Run("unset", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + assertFalse(t, nullable.IsNull()) + }) +} + +// TestIsSet tests the IsSet method +func TestIsSet(t *testing.T) { + t.Parallel() + t.Run("with value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + assertTrue(t, nullable.IsSet()) + }) + + t.Run("with nil pointer", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + assertTrue(t, nullable.IsSet()) + }) + + t.Run("unset", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + assertFalse(t, nullable.IsSet()) + }) +} + +// TestGet tests the Get method +func TestGet(t *testing.T) { + t.Parallel() + t.Run("with string value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "test", got) + }) + + t.Run("with nil pointer", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "", got) // zero value + }) + + t.Run("unset", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + + got, ok := nullable.GetOrZero() + assertFalse(t, ok) + assertEqual(t, "", got) // zero value + }) + + t.Run("with slice value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom([]string{"a", "b"})) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b"}, got) + }) + + t.Run("with nil slice pointer", func(t *testing.T) { + t.Parallel() + nullable := From[[]string](nil) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertNil(t, got) // zero value for slice is nil + }) +} + +// TestPointer tests the Pointer method +func TestPointer(t *testing.T) { + t.Parallel() + t.Run("with value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + + ptr, ok := nullable.Get() + assertTrue(t, ok) + assertNotNil(t, ptr) + assertEqual(t, "test", *ptr) + }) + + t.Run("with nil pointer", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + + ptr, ok := nullable.Get() + assertTrue(t, ok) + assertNil(t, ptr) + }) + + t.Run("unset", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + + ptr, ok := nullable.Get() + assertFalse(t, ok) + assertNil(t, ptr) + }) +} + +// TestSet tests the Set method +func TestSet(t *testing.T) { + t.Parallel() + t.Run("set string value", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + + // Initially unset + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // Unset is not null + + // Set a value + nullable.Set(ptrFrom("test")) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "test", got) + }) + + t.Run("set int value", func(t *testing.T) { + t.Parallel() + nullable := OptionalNullable[int]{} + + nullable.Set(ptrFrom(42)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 42, got) + }) + + t.Run("set slice value", func(t *testing.T) { + t.Parallel() + nullable := OptionalNullable[[]string]{} + + slice := []string{"a", "b"} + nullable.Set(ptrFrom(slice)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b"}, got) + }) + + t.Run("set empty slice", func(t *testing.T) { + t.Parallel() + nullable := OptionalNullable[[]string]{} + + slice := []string{} + nullable.Set(ptrFrom(slice)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{}, got) + }) + + t.Run("overwrite existing value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("original")) + + // Verify original value + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "original", got) + + // Set new value + nullable.Set(ptrFrom("new")) + + got, ok = nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "new", got) + }) +} + +// TestUnset tests the Unset method +func TestUnset(t *testing.T) { + t.Parallel() + t.Run("unset from value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + + // Initially set + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + // Unset + nullable.Unset() + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // After unset is not null + // Value is now internal to the map implementation + + got, ok := nullable.GetOrZero() + assertFalse(t, ok) + assertEqual(t, "", got) // zero value + }) + + t.Run("unset from nil", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + + // Initially set to nil + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) // Set to nil should be null + + // Unset + nullable.Unset() + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // After unset is not null + }) + + t.Run("unset already unset", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + + // Initially unset + assertFalse(t, nullable.IsSet()) + + // Unset again + nullable.Unset() + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) // Empty map is not null + }) +} + +// TestMarshalJSON tests JSON marshaling +func TestMarshalJSON(t *testing.T) { + t.Parallel() + t.Run("marshal string value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `"test"`, string(data)) + }) + + t.Run("marshal int value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom(42)) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `42`, string(data)) + }) + + t.Run("marshal nil value", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `null`, string(data)) + }) + + t.Run("marshal slice value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom([]string{"a", "b", "c"})) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `["a","b","c"]`, string(data)) + }) + + t.Run("marshal empty slice", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom([]string{})) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `[]`, string(data)) + }) + + t.Run("marshal struct value", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom(TestStruct{Name: "John", Age: 30})) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `{"name":"John","age":30}`, string(data)) + }) + + // Note: Unset values are not tested here because the current implementation + // doesn't handle unset fields in marshaling (see TODO in the code) +} + +// TestUnmarshalJSON tests JSON unmarshaling +func TestUnmarshalJSON(t *testing.T) { + t.Parallel() + t.Run("unmarshal string value", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + err := json.Unmarshal([]byte(`"test"`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "test", got) + }) + + t.Run("unmarshal int value", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[int] + err := json.Unmarshal([]byte(`42`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 42, got) + }) + + t.Run("unmarshal null value", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + err := json.Unmarshal([]byte(`null`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "", got) // zero value + }) + + t.Run("unmarshal slice value", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[[]string] + err := json.Unmarshal([]byte(`["a","b","c"]`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b", "c"}, got) + }) + + t.Run("unmarshal empty slice", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[[]string] + err := json.Unmarshal([]byte(`[]`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{}, got) + }) + + t.Run("unmarshal struct value", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[TestStruct] + err := json.Unmarshal([]byte(`{"name":"John","age":30}`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, TestStruct{Name: "John", Age: 30}, got) + }) + + t.Run("unmarshal invalid JSON", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + err := json.Unmarshal([]byte(`invalid`), &nullable) + assertError(t, err) + + // Ensure the nullable remains unset after error + assertFalse(t, nullable.IsSet()) + }) + + t.Run("unmarshal invalid JSON for int", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[int] + err := json.Unmarshal([]byte(`"not_a_number"`), &nullable) + assertError(t, err) + + // Ensure the nullable remains unset after error + assertFalse(t, nullable.IsSet()) + }) + + t.Run("unmarshal malformed JSON", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[TestStruct] + err := json.Unmarshal([]byte(`{invalid json`), &nullable) + assertError(t, err) + + // Ensure the nullable remains unset after error + assertFalse(t, nullable.IsSet()) + }) +} + +// TestJSONRoundTrip tests marshaling and unmarshaling together +func TestJSONRoundTrip(t *testing.T) { + t.Parallel() + t.Run("string value round trip", func(t *testing.T) { + t.Parallel() + nullable1 := From(ptrFrom("test value")) + + // Marshal + data, err := json.Marshal(nullable1) + mustNoError(t, err) + + // Unmarshal + var nullable2 OptionalNullable[string] + err = json.Unmarshal(data, &nullable2) + mustNoError(t, err) + + // Compare + assertEqual(t, nullable1.IsSet(), nullable2.IsSet()) + assertEqual(t, nullable1.IsNull(), nullable2.IsNull()) + + got1, ok1 := nullable1.GetOrZero() + got2, ok2 := nullable2.GetOrZero() + assertEqual(t, ok1, ok2) + assertEqual(t, got1, got2) + }) + + t.Run("nil value round trip", func(t *testing.T) { + t.Parallel() + nullable1 := From[string](nil) + + // Marshal + data, err := json.Marshal(nullable1) + mustNoError(t, err) + + // Unmarshal + var nullable2 OptionalNullable[string] + err = json.Unmarshal(data, &nullable2) + mustNoError(t, err) + + // Compare + assertEqual(t, nullable1.IsSet(), nullable2.IsSet()) + assertEqual(t, nullable1.IsNull(), nullable2.IsNull()) + + got1, ok1 := nullable1.GetOrZero() + got2, ok2 := nullable2.GetOrZero() + assertEqual(t, ok1, ok2) + assertEqual(t, got1, got2) + }) + + t.Run("slice round trip", func(t *testing.T) { + t.Parallel() + nullable1 := From(ptrFrom([]string{"a", "b", "c"})) + + // Marshal + data, err := json.Marshal(nullable1) + mustNoError(t, err) + + // Unmarshal + var nullable2 OptionalNullable[[]string] + err = json.Unmarshal(data, &nullable2) + mustNoError(t, err) + + // Compare + assertEqual(t, nullable1.IsSet(), nullable2.IsSet()) + assertEqual(t, nullable1.IsNull(), nullable2.IsNull()) + + got1, ok1 := nullable1.GetOrZero() + got2, ok2 := nullable2.GetOrZero() + assertEqual(t, ok1, ok2) + assertEqual(t, got1, got2) + }) +} + +// TestJSONToJSONRoundTrip tests starting with JSON and ensuring we can serialize back to the same JSON +func TestJSONToJSONRoundTrip(t *testing.T) { + t.Parallel() + t.Run("string value JSON round trip", func(t *testing.T) { + t.Parallel() + originalJSON := `"hello world"` + + // Unmarshal from JSON + var nullable OptionalNullable[string] + err := json.Unmarshal([]byte(originalJSON), &nullable) + mustNoError(t, err) + + // Verify state + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "hello world", got) + + // Marshal back to JSON + resultJSON, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, originalJSON, string(resultJSON)) + }) + + t.Run("null value JSON round trip", func(t *testing.T) { + t.Parallel() + originalJSON := `null` + + // Unmarshal from JSON + var nullable OptionalNullable[string] + err := json.Unmarshal([]byte(originalJSON), &nullable) + mustNoError(t, err) + + // Verify state + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "", got) // zero value + + // Marshal back to JSON + resultJSON, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, originalJSON, string(resultJSON)) + }) + + t.Run("int value JSON round trip", func(t *testing.T) { + t.Parallel() + originalJSON := `42` + + // Unmarshal from JSON + var nullable OptionalNullable[int] + err := json.Unmarshal([]byte(originalJSON), &nullable) + mustNoError(t, err) + + // Verify state + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 42, got) + + // Marshal back to JSON + resultJSON, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, originalJSON, string(resultJSON)) + }) + + t.Run("slice value JSON round trip", func(t *testing.T) { + t.Parallel() + originalJSON := `["a","b","c"]` + + // Unmarshal from JSON + var nullable OptionalNullable[[]string] + err := json.Unmarshal([]byte(originalJSON), &nullable) + mustNoError(t, err) + + // Verify state + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b", "c"}, got) + + // Marshal back to JSON + resultJSON, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, originalJSON, string(resultJSON)) + }) + + t.Run("empty slice JSON round trip", func(t *testing.T) { + t.Parallel() + originalJSON := `[]` + + // Unmarshal from JSON + var nullable OptionalNullable[[]string] + err := json.Unmarshal([]byte(originalJSON), &nullable) + mustNoError(t, err) + + // Verify state + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{}, got) + + // Marshal back to JSON + resultJSON, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, originalJSON, string(resultJSON)) + }) + + t.Run("struct value JSON round trip", func(t *testing.T) { + t.Parallel() + originalJSON := `{"name":"Alice","age":25}` + + // Unmarshal from JSON + var nullable OptionalNullable[TestStruct] + err := json.Unmarshal([]byte(originalJSON), &nullable) + mustNoError(t, err) + + // Verify state + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, TestStruct{Name: "Alice", Age: 25}, got) + + // Marshal back to JSON + resultJSON, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, originalJSON, string(resultJSON)) + }) +} + +// TestContainerStates tests comprehensive state detection and serialization with TestContainer +func TestContainerStates(t *testing.T) { + t.Parallel() + t.Run("all fields set to values", func(t *testing.T) { + t.Parallel() + container := TestContainer{ + StringField: From(ptrFrom("hello")), + IntField: From(ptrFrom(42)), + SliceField: From(ptrFrom([]string{"a", "b"})), + StructField: From(ptrFrom(TestStruct{Name: "John", Age: 30})), + } + + // Verify all fields are set and not null + assertTrue(t, container.StringField.IsSet()) + assertFalse(t, container.StringField.IsNull()) + assertTrue(t, container.IntField.IsSet()) + assertFalse(t, container.IntField.IsNull()) + assertTrue(t, container.SliceField.IsSet()) + assertFalse(t, container.SliceField.IsNull()) + assertTrue(t, container.StructField.IsSet()) + assertFalse(t, container.StructField.IsNull()) + + // Verify values + stringVal, ok := container.StringField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "hello", stringVal) + + intVal, ok := container.IntField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 42, intVal) + + sliceVal, ok := container.SliceField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b"}, sliceVal) + + structVal, ok := container.StructField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, TestStruct{Name: "John", Age: 30}, structVal) + + // Test JSON serialization + data, err := json.Marshal(container) + mustNoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + mustNoError(t, err) + + assertEqual(t, "hello", result["string_field"]) + assertEqual(t, float64(42), result["int_field"]) // JSON numbers are float64 + assertEqual(t, []interface{}{"a", "b"}, result["slice_field"]) + structResult := result["struct_field"].(map[string]interface{}) + assertEqual(t, "John", structResult["name"]) + assertEqual(t, float64(30), structResult["age"]) + }) + + t.Run("all fields set to nil", func(t *testing.T) { + t.Parallel() + container := TestContainer{ + StringField: From[string](nil), + IntField: From[int](nil), + SliceField: From[[]string](nil), + StructField: From[TestStruct](nil), + } + + // Verify all fields are set but null + assertTrue(t, container.StringField.IsSet()) + assertTrue(t, container.StringField.IsNull()) + assertTrue(t, container.IntField.IsSet()) + assertTrue(t, container.IntField.IsNull()) + assertTrue(t, container.SliceField.IsSet()) + assertTrue(t, container.SliceField.IsNull()) + assertTrue(t, container.StructField.IsSet()) + assertTrue(t, container.StructField.IsNull()) + + // Verify GetOrZero() behavior for nil values + stringVal, ok := container.StringField.GetOrZero() + assertTrue(t, ok) // set to nil still returns true + assertEqual(t, "", stringVal) // zero value + + intVal, ok := container.IntField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 0, intVal) // zero value + + sliceVal, ok := container.SliceField.GetOrZero() + assertTrue(t, ok) + assertNil(t, sliceVal) // zero value for slice is nil + + structVal, ok := container.StructField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, TestStruct{}, structVal) // zero value + + // Test JSON serialization - all should be null + data, err := json.Marshal(container) + mustNoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + mustNoError(t, err) + + assertNil(t, result["string_field"]) + assertNil(t, result["int_field"]) + assertNil(t, result["slice_field"]) + assertNil(t, result["struct_field"]) + }) + + t.Run("all fields unset", func(t *testing.T) { + t.Parallel() + container := TestContainer{} + + // Verify all fields are unset + assertFalse(t, container.StringField.IsSet()) + assertFalse(t, container.StringField.IsNull()) // unset is not null in new implementation + assertFalse(t, container.IntField.IsSet()) + assertFalse(t, container.IntField.IsNull()) + assertFalse(t, container.SliceField.IsSet()) + assertFalse(t, container.SliceField.IsNull()) + assertFalse(t, container.StructField.IsSet()) + assertFalse(t, container.StructField.IsNull()) + + // Verify GetOrZero() behavior for unset values + stringVal, ok := container.StringField.GetOrZero() + assertFalse(t, ok) // unset returns false + assertEqual(t, "", stringVal) // zero value + + intVal, ok := container.IntField.GetOrZero() + assertFalse(t, ok) + assertEqual(t, 0, intVal) // zero value + + sliceVal, ok := container.SliceField.GetOrZero() + assertFalse(t, ok) + assertNil(t, sliceVal) // zero value + + structVal, ok := container.StructField.GetOrZero() + assertFalse(t, ok) + assertEqual(t, TestStruct{}, structVal) // zero value + + // Test JSON serialization - unset fields should be omitted due to omitempty + data, err := json.Marshal(container) + mustNoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + mustNoError(t, err) + + // With omitempty, unset fields should not appear in JSON + assertNotContains(t, result, "string_field") + assertNotContains(t, result, "int_field") + assertNotContains(t, result, "slice_field") + assertNotContains(t, result, "struct_field") + }) + + t.Run("slice field states: nil vs unset vs empty vs set", func(t *testing.T) { + t.Parallel() + // Test all possible slice states + nilSlice := TestContainer{ + SliceField: From[[]string](nil), // explicitly set to nil + } + unsetSlice := TestContainer{} // unset + emptySlice := TestContainer{ + SliceField: From(ptrFrom([]string{})), // empty slice + } + setSlice := TestContainer{ + SliceField: From(ptrFrom([]string{"a", "b"})), // slice with values + } + + // Verify nil slice + assertTrue(t, nilSlice.SliceField.IsSet()) + assertTrue(t, nilSlice.SliceField.IsNull()) + val, ok := nilSlice.SliceField.GetOrZero() + assertTrue(t, ok) + assertNil(t, val) + + // Verify unset slice + assertFalse(t, unsetSlice.SliceField.IsSet()) + assertFalse(t, unsetSlice.SliceField.IsNull()) // Unset is not null + val, ok = unsetSlice.SliceField.GetOrZero() + assertFalse(t, ok) + assertNil(t, val) + + // Verify empty slice + assertTrue(t, emptySlice.SliceField.IsSet()) + assertFalse(t, emptySlice.SliceField.IsNull()) + val, ok = emptySlice.SliceField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{}, val) + + // Verify set slice + assertTrue(t, setSlice.SliceField.IsSet()) + assertFalse(t, setSlice.SliceField.IsNull()) + val, ok = setSlice.SliceField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, []string{"a", "b"}, val) + + // Test JSON serialization for each state + nilData, err := json.Marshal(nilSlice) + mustNoError(t, err) + assertContains(t, string(nilData), `"slice_field":null`) + + unsetData, err := json.Marshal(unsetSlice) + mustNoError(t, err) + assertNotContains(t, string(unsetData), "slice_field") // omitted due to omitempty + + emptyData, err := json.Marshal(emptySlice) + mustNoError(t, err) + assertContains(t, string(emptyData), `"slice_field":[]`) + + setData, err := json.Marshal(setSlice) + mustNoError(t, err) + assertContains(t, string(setData), `"slice_field":["a","b"]`) + }) + + t.Run("mixed states container", func(t *testing.T) { + t.Parallel() + container := TestContainer{ + StringField: From(ptrFrom("hello")), // set to value + IntField: From[int](nil), // set to nil + StructField: From(ptrFrom(TestStruct{Name: "Alice", Age: 25})), // set to value + } + + // Verify states + assertTrue(t, container.StringField.IsSet()) + assertFalse(t, container.StringField.IsNull()) + + assertTrue(t, container.IntField.IsSet()) + assertTrue(t, container.IntField.IsNull()) + + assertFalse(t, container.SliceField.IsSet()) + assertFalse(t, container.SliceField.IsNull()) // Unset is not null + + assertTrue(t, container.StructField.IsSet()) + assertFalse(t, container.StructField.IsNull()) + + // Test JSON serialization + data, err := json.Marshal(container) + mustNoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + mustNoError(t, err) + + assertEqual(t, "hello", result["string_field"]) + assertNil(t, result["int_field"]) + assertNotContains(t, result, "slice_field") // unset, so omitted + structResult := result["struct_field"].(map[string]interface{}) + assertEqual(t, "Alice", structResult["name"]) + assertEqual(t, float64(25), structResult["age"]) + }) + + t.Run("JSON unmarshaling preserves states", func(t *testing.T) { + t.Parallel() + // JSON with some fields missing, some null, some with values + jsonData := `{ + "string_field": "test", + "int_field": null, + "struct_field": {"name": "Bob", "age": 35} + }` + + var container TestContainer + err := json.Unmarshal([]byte(jsonData), &container) + mustNoError(t, err) + + // string_field: present with value + assertTrue(t, container.StringField.IsSet()) + assertFalse(t, container.StringField.IsNull()) + stringVal, ok := container.StringField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "test", stringVal) + + // int_field: present but null + assertTrue(t, container.IntField.IsSet()) + assertTrue(t, container.IntField.IsNull()) + intVal, ok := container.IntField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 0, intVal) // zero value + + // slice_field: missing from JSON, should remain unset + assertFalse(t, container.SliceField.IsSet()) + assertFalse(t, container.SliceField.IsNull()) // Unset is not null + sliceVal, ok := container.SliceField.GetOrZero() + assertFalse(t, ok) + assertNil(t, sliceVal) + + // struct_field: present with value + assertTrue(t, container.StructField.IsSet()) + assertFalse(t, container.StructField.IsNull()) + structVal, ok := container.StructField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, TestStruct{Name: "Bob", Age: 35}, structVal) + }) +} + +// TestNilVsUnsetDistinction tests the key feature of distinguishing nil from unset +func TestNilVsUnsetDistinction(t *testing.T) { + t.Parallel() + t.Run("explicit nil vs unset", func(t *testing.T) { + t.Parallel() + // Explicitly set to nil + explicitNil := From[string](nil) + + // Unset + var unset OptionalNullable[string] + + // Both are null, but only one is set + assertTrue(t, explicitNil.IsNull()) + assertTrue(t, explicitNil.IsSet()) + + assertFalse(t, unset.IsNull()) // Unset is not null + assertFalse(t, unset.IsSet()) + + // Get behavior differs + got1, ok1 := explicitNil.GetOrZero() + got2, ok2 := unset.GetOrZero() + + assertTrue(t, ok1) // explicitly set to nil returns true + assertFalse(t, ok2) // unset returns false + assertEqual(t, "", got1) // both return zero value + assertEqual(t, "", got2) + + // Get behavior differs + ptr1, ok1 := explicitNil.Get() + ptr2, ok2 := unset.Get() + + assertTrue(t, ok1) // explicitly set to nil returns true + assertFalse(t, ok2) // unset returns false + assertNil(t, ptr1) // both return nil pointer + assertNil(t, ptr2) + }) + + t.Run("empty slice vs nil slice vs unset", func(t *testing.T) { + t.Parallel() + // Empty slice + emptyNullable := From(ptrFrom([]string{})) + + // Nil slice + nilNullable := From[[]string](nil) + + // Unset + var unsetNullable OptionalNullable[[]string] + + // All have different characteristics + assertTrue(t, emptyNullable.IsSet()) + assertFalse(t, emptyNullable.IsNull()) + + assertTrue(t, nilNullable.IsSet()) + assertTrue(t, nilNullable.IsNull()) + + assertFalse(t, unsetNullable.IsSet()) + assertFalse(t, unsetNullable.IsNull()) // Unset is not null + + // Get behavior + got1, ok1 := emptyNullable.GetOrZero() + got2, ok2 := nilNullable.GetOrZero() + got3, ok3 := unsetNullable.GetOrZero() + + assertTrue(t, ok1) + assertEqual(t, []string{}, got1) + + assertTrue(t, ok2) + assertNil(t, got2) + + assertFalse(t, ok3) + assertNil(t, got3) + }) +} + +// TestJSONOmitEmpty tests behavior with omitempty tag +func TestJSONOmitEmpty(t *testing.T) { + t.Parallel() + t.Run("marshal with omitempty", func(t *testing.T) { + t.Parallel() + // Test container with various nullable states + container := TestContainer{ + StringField: From(ptrFrom("test")), + IntField: From(ptrFrom(42)), + StructField: From[TestStruct](nil), // explicitly nil + } + + data, err := json.Marshal(container) + mustNoError(t, err) + + // Parse back to verify structure + var result map[string]interface{} + err = json.Unmarshal(data, &result) + mustNoError(t, err) + + // Should contain set fields + assertContains(t, result, "string_field") + assertContains(t, result, "int_field") + assertContains(t, result, "struct_field") + + // Should not contain unset field (due to omitempty) + // Note: This depends on how the marshaling handles unset fields + // The current implementation doesn't handle this case properly (see TODO) + }) + + t.Run("unmarshal missing fields", func(t *testing.T) { + t.Parallel() + // JSON with some fields missing + jsonData := `{"string_field": "test", "int_field": null}` + + var container TestContainer + err := json.Unmarshal([]byte(jsonData), &container) + mustNoError(t, err) + + // Present fields should be set + assertTrue(t, container.StringField.IsSet()) + assertFalse(t, container.StringField.IsNull()) + got, ok := container.StringField.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "test", got) + + // Null field should be set to nil + assertTrue(t, container.IntField.IsSet()) + assertTrue(t, container.IntField.IsNull()) + + // Missing fields should remain unset + assertFalse(t, container.SliceField.IsSet()) + assertFalse(t, container.StructField.IsSet()) + }) +} + +// TestEdgeCases tests various edge cases +func TestEdgeCases(t *testing.T) { + t.Parallel() + t.Run("zero values", func(t *testing.T) { + t.Parallel() + // Test with zero values that are not nil + intNullable := From(ptrFrom(0)) + stringNullable := From(ptrFrom("")) + + assertTrue(t, intNullable.IsSet()) + assertFalse(t, intNullable.IsNull()) + got, ok := intNullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, 0, got) + + assertTrue(t, stringNullable.IsSet()) + assertFalse(t, stringNullable.IsNull()) + got2, ok2 := stringNullable.GetOrZero() + assertTrue(t, ok2) + assertEqual(t, "", got2) + }) + + t.Run("pointer to pointer", func(t *testing.T) { + t.Parallel() + // Test with pointer to pointer type + inner := "test" + nullable := From(ptrFrom(&inner)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, &inner, got) + assertEqual(t, "test", *got) + }) + + t.Run("complex struct", func(t *testing.T) { + t.Parallel() + complexStruct := struct { + Name string + Values []int + Metadata map[string]string + }{ + Name: "complex", + Values: []int{1, 2, 3}, + Metadata: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + + nullable := From(ptrFrom(complexStruct)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, complexStruct, got) + }) +} + +// TestDoublePointers tests comprehensive double pointer scenarios +func TestDoublePointers(t *testing.T) { + t.Parallel() + + t.Run("string double pointer with value", func(t *testing.T) { + t.Parallel() + inner := "hello world" + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertEqual(t, &inner, got) + assertEqual(t, "hello world", *got) + }) + + t.Run("int double pointer with value", func(t *testing.T) { + t.Parallel() + inner := 42 + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertEqual(t, &inner, got) + assertEqual(t, 42, *got) + }) + + t.Run("double pointer to nil", func(t *testing.T) { + t.Parallel() + var ptr *string = nil + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertNil(t, got) + }) + + t.Run("nil double pointer", func(t *testing.T) { + t.Parallel() + nullable := From[*string](nil) + + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertNil(t, got) // zero value for **string is nil + }) + + t.Run("unset double pointer", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[*string] + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertFalse(t, ok) + assertNil(t, got) // zero value for **string is nil + }) + + t.Run("double pointer modification", func(t *testing.T) { + t.Parallel() + inner := "original" + ptr := &inner + nullable := From(ptrFrom(ptr)) + + // Verify original value + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "original", *got) + + // Modify through double pointer + *got = "modified" + assertEqual(t, "modified", inner) + assertEqual(t, "modified", *got) + }) + + t.Run("double pointer to struct", func(t *testing.T) { + t.Parallel() + inner := TestStruct{Name: "Alice", Age: 30} + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertEqual(t, TestStruct{Name: "Alice", Age: 30}, *got) + + // Modify through double pointer + (*got).Name = "Bob" + assertEqual(t, "Bob", inner.Name) + assertEqual(t, "Bob", (*got).Name) + }) + + t.Run("double pointer to slice", func(t *testing.T) { + t.Parallel() + inner := []string{"a", "b", "c"} + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertEqual(t, []string{"a", "b", "c"}, *got) + + // Modify through double pointer + *got = append(*got, "d") + assertEqual(t, []string{"a", "b", "c", "d"}, inner) + assertEqual(t, []string{"a", "b", "c", "d"}, *got) + }) + + t.Run("double pointer to empty slice", func(t *testing.T) { + t.Parallel() + inner := []string{} + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertEqual(t, []string{}, *got) + }) + + t.Run("double pointer to nil slice", func(t *testing.T) { + t.Parallel() + var inner []string = nil + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr, got) + assertNil(t, *got) + }) + + t.Run("double pointer JSON marshaling", func(t *testing.T) { + t.Parallel() + inner := "json test" + ptr := &inner + nullable := From(ptrFrom(ptr)) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `"json test"`, string(data)) + }) + + t.Run("double pointer JSON unmarshaling", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[*string] + err := json.Unmarshal([]byte(`"json test"`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertNotNil(t, got) + assertEqual(t, "json test", *got) + }) + + t.Run("double pointer JSON null marshaling", func(t *testing.T) { + t.Parallel() + nullable := From[*string](nil) + + data, err := json.Marshal(nullable) + mustNoError(t, err) + assertEqual(t, `null`, string(data)) + }) + + t.Run("double pointer JSON null unmarshaling", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[*string] + err := json.Unmarshal([]byte(`null`), &nullable) + mustNoError(t, err) + + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertNil(t, got) + }) + + t.Run("double pointer round trip", func(t *testing.T) { + t.Parallel() + inner := "round trip test" + ptr := &inner + nullable1 := From(ptrFrom(ptr)) + + // Marshal + data, err := json.Marshal(nullable1) + mustNoError(t, err) + + // Unmarshal + var nullable2 OptionalNullable[*string] + err = json.Unmarshal(data, &nullable2) + mustNoError(t, err) + + // Compare states + assertEqual(t, nullable1.IsSet(), nullable2.IsSet()) + assertEqual(t, nullable1.IsNull(), nullable2.IsNull()) + + got1, ok1 := nullable1.GetOrZero() + got2, ok2 := nullable2.GetOrZero() + assertEqual(t, ok1, ok2) + + // Values should be equal + assertEqual(t, *got1, *got2) + }) + + t.Run("triple pointer", func(t *testing.T) { + t.Parallel() + inner := "triple" + ptr1 := &inner + ptr2 := &ptr1 + nullable := From(ptrFrom(ptr2)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, ptr2, got) + assertEqual(t, ptr1, *got) + assertEqual(t, "triple", **got) + }) + + t.Run("double pointer set and unset", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[*string] + + // Initially unset + assertFalse(t, nullable.IsSet()) + + // Set to double pointer + inner := "set test" + ptr := &inner + nullable.Set(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "set test", *got) + + // Set to nil + nullable.Set(nil) + + assertTrue(t, nullable.IsSet()) + assertTrue(t, nullable.IsNull()) + + got, ok = nullable.GetOrZero() + assertTrue(t, ok) + assertNil(t, got) + + // Unset + nullable.Unset() + + assertFalse(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok = nullable.GetOrZero() + assertFalse(t, ok) + assertNil(t, got) + }) + + t.Run("double pointer Get method", func(t *testing.T) { + t.Parallel() + inner := "get test" + ptr := &inner + nullable := From(ptrFrom(ptr)) + + // Test Get method + gotPtr, ok := nullable.Get() + assertTrue(t, ok) + assertNotNil(t, gotPtr) + assertEqual(t, ptr, *gotPtr) + assertEqual(t, "get test", **gotPtr) + + // Test with nil + nilNullable := From[*string](nil) + gotPtr, ok = nilNullable.Get() + assertTrue(t, ok) + assertNil(t, gotPtr) + + // Test with unset + var unsetNullable OptionalNullable[*string] + gotPtr, ok = unsetNullable.Get() + assertFalse(t, ok) + assertNil(t, gotPtr) + }) + + t.Run("double pointer zero values", func(t *testing.T) { + t.Parallel() + // Test with zero value string + inner := "" + ptr := &inner + nullable := From(ptrFrom(ptr)) + + assertTrue(t, nullable.IsSet()) + assertFalse(t, nullable.IsNull()) + + got, ok := nullable.GetOrZero() + assertTrue(t, ok) + assertEqual(t, "", *got) + + // Test with zero value int + innerInt := 0 + ptrInt := &innerInt + nullableInt := From(ptrFrom(ptrInt)) + + assertTrue(t, nullableInt.IsSet()) + assertFalse(t, nullableInt.IsNull()) + + gotInt, okInt := nullableInt.GetOrZero() + assertTrue(t, okInt) + assertEqual(t, 0, *gotInt) + }) +} + +// TestAsOptionalNullable tests the AsOptionalNullable helper function +func TestAsOptionalNullable(t *testing.T) { + t.Parallel() + + t.Run("with nullable string", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, "test", value) + }) + + t.Run("with nullable int", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom(42)) + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, 42, value) + }) + + t.Run("with nullable nil", func(t *testing.T) { + t.Parallel() + nullable := From[string](nil) + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertNil(t, value) + }) + + t.Run("with unset nullable", func(t *testing.T) { + t.Parallel() + var nullable OptionalNullable[string] + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertFalse(t, ok) + assertNil(t, result) + }) + + t.Run("with non-nullable string", func(t *testing.T) { + t.Parallel() + regularString := "not nullable" + reflectValue := reflect.ValueOf(regularString) + + result, ok := AsOptionalNullable(reflectValue) + assertFalse(t, ok) + assertNil(t, result) + }) + + t.Run("with non-nullable int", func(t *testing.T) { + t.Parallel() + regularInt := 42 + reflectValue := reflect.ValueOf(regularInt) + + result, ok := AsOptionalNullable(reflectValue) + assertFalse(t, ok) + assertNil(t, result) + }) + + t.Run("with non-nullable map", func(t *testing.T) { + t.Parallel() + regularMap := map[string]int{"key": 42} + reflectValue := reflect.ValueOf(regularMap) + + result, ok := AsOptionalNullable(reflectValue) + assertFalse(t, ok) + assertNil(t, result) + }) + + t.Run("with non-nullable struct", func(t *testing.T) { + t.Parallel() + regularStruct := TestStruct{Name: "test", Age: 30} + reflectValue := reflect.ValueOf(regularStruct) + + result, ok := AsOptionalNullable(reflectValue) + assertFalse(t, ok) + assertNil(t, result) + }) + + t.Run("with nullable double pointer", func(t *testing.T) { + t.Parallel() + inner := "test" + ptr := &inner + nullable := From(ptrFrom(ptr)) + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, ptr, value) + assertEqual(t, "test", *value.(*string)) + }) + + t.Run("with nullable slice", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom([]string{"a", "b", "c"})) + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, []string{"a", "b", "c"}, value) + }) + + t.Run("with nullable struct", func(t *testing.T) { + t.Parallel() + testStruct := TestStruct{Name: "Alice", Age: 25} + nullable := From(ptrFrom(testStruct)) + reflectValue := reflect.ValueOf(nullable) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, testStruct, value) + }) + + t.Run("with pointer to nullable", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + ptrToNullable := &nullable + reflectValue := reflect.ValueOf(ptrToNullable) + + // This should work since the pointer to nullable still contains a nullable + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, "test", value) + }) + + t.Run("with interface containing nullable", func(t *testing.T) { + t.Parallel() + nullable := From(ptrFrom("test")) + var iface interface{} = nullable + reflectValue := reflect.ValueOf(iface) + + result, ok := AsOptionalNullable(reflectValue) + assertTrue(t, ok) + assertNotNil(t, result) + + value, isSet := result.GetUntyped() + assertTrue(t, isSet) + assertEqual(t, "test", value) + }) +} diff --git a/internal/sdk/retry/config.go b/internal/sdk/retry/config.go new file mode 100644 index 0000000..3201de3 --- /dev/null +++ b/internal/sdk/retry/config.go @@ -0,0 +1,172 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package retry + +import ( + "errors" + "net/http" + "strconv" + "time" +) + +// BackoffStrategy defines the parameters for exponential backoff. This can be +// used to drive a retry loop for example. +type BackoffStrategy struct { + InitialInterval int + MaxInterval int + Exponent float64 + MaxElapsedTime int +} + +// Config configures a retry policy. +type Config struct { + // Strategy sets the algorithm to use for a retry loop. It can be one of: + // - "backoff": retry with exponential backoff and random jitter. + // - "attempt-count-backoff": retry with exponential backoff up to MaxRetries. + // - "none" or "": disables retries. + Strategy string + Backoff *BackoffStrategy + RetryConnectionErrors bool + MaxRetries *int +} + +// PermanentError is an error that signals that some operation has terminally +// failed and should not be retried. +type PermanentError struct { + cause error +} + +// Permanent creates a PermanentError that signals to a retry loop that it +// should stop retrying an operation and return the underlying error. +func Permanent(cause error) error { + if IsPermanentError(cause) { + return cause + } + + return &PermanentError{ + cause: cause, + } +} + +func (e *PermanentError) Error() string { + return e.cause.Error() +} + +func (e *PermanentError) Unwrap() error { + return e.cause +} + +// TemporaryError represents a retryable error and signals to a retry loop that +// an operation may be retried with an optional wait interval. +type TemporaryError struct { + wait time.Duration + message string +} + +// Temporary creates a TemporaryError that signals to a retry loop that an +// operation can be retried. The error may also carry details about how long to +// wait before retrying. This wait interval may be used to override the retry +// policy in use. +func Temporary(message string) error { + return &TemporaryError{ + message: message, + } +} + +// TemporaryFromResponse creates a TemporaryError similar to Temporary but +// additionally parses the Retry-After header from a response to determine the +// wait interval before the next retry attempt. +func TemporaryFromResponse(message string, res *http.Response) error { + return &TemporaryError{ + wait: retryIntervalFromResponse(res), + message: message, + } +} + +func (e *TemporaryError) Error() string { + return e.message +} + +// RetryAfter returns the time to wait before retrying the request. The zero +// value should be interpreted by retry loops to mean they should fallback on +// their default policy whether expenonential, constant backoff or something +// else. It does not mean that an operation should be retried immediately. +func (e *TemporaryError) RetryAfter() time.Duration { + return e.wait +} + +func retryIntervalFromResponse(res *http.Response) time.Duration { + if res == nil { + return 0 + } + + retryAfterMsVal := res.Header.Get("retry-after-ms") + if retryAfterMsVal != "" { + parsedMs, err := strconv.ParseInt(retryAfterMsVal, 10, 64) + if err == nil && parsedMs >= 0 { + return time.Duration(parsedMs) * time.Millisecond + } + } + + retryVal := res.Header.Get("retry-after") + if retryVal == "" { + return 0 + } + + parsedNumber, err := strconv.ParseInt(retryVal, 10, 64) + if err == nil { + if parsedNumber < 0 { + return 0 + } else { + return time.Duration(parsedNumber) * time.Second + } + } + + parsedDate, err := time.Parse(time.RFC1123, retryVal) + if err == nil { + delta := time.Until(parsedDate) + if delta < 0 { + return 0 + } else { + return delta + } + } + + return 0 +} + +// IsPermanentError returns true if an error value is or contains a +// PermanentError in its chain of errors. +func IsPermanentError(err error) bool { + if err == nil { + return false + } + + var pe *PermanentError + return errors.As(err, &pe) +} + +// IsTemporaryError returns true if an error value is or contains a +// TemporaryError in its chain of errors. +func IsTemporaryError(err error) bool { + if err == nil { + return false + } + + var pe *TemporaryError + return errors.As(err, &pe) +} diff --git a/internal/sdk/sdkinternal/config/sdkconfiguration.go b/internal/sdk/sdkinternal/config/sdkconfiguration.go new file mode 100644 index 0000000..970c51b --- /dev/null +++ b/internal/sdk/sdkinternal/config/sdkconfiguration.go @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package config + +import ( + "context" + "net/http" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/globals" +) + +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +type SDKConfiguration struct { + Client HTTPClient + Security func(context.Context) (interface{}, error) + ServerURL string + ServerIndex int + ServerList []string + UserAgent string + SDKVersion string + GenVersion string + OpenAPIDocVersion string + Globals globals.Globals + RetryConfig *retry.Config + Timeout *time.Duration +} + +func (c *SDKConfiguration) GetServerDetails() (string, map[string]string) { + if c.ServerURL != "" { + return c.ServerURL, nil + } + + return c.ServerList[c.ServerIndex], nil +} diff --git a/internal/sdk/sdkinternal/globals/globals.go b/internal/sdk/sdkinternal/globals/globals.go new file mode 100644 index 0000000..a57a41e --- /dev/null +++ b/internal/sdk/sdkinternal/globals/globals.go @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package globals + +import ( + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" +) + +type Globals struct { + // Which version of the API to use. Defaults to v1beta (the only version covering the full interactions surface). + APIVersion *string `default:"v1beta" pathParam:"style=simple,explode=false,name=api_version"` + // Interactions API revision to request. Omitted by default (matching the official SDKs), so the service serves its current revision. + APIRevision *string `header:"style=simple,explode=false,name=Api-Revision"` + // Quota project header to send with Google GenAI API requests. + UserProject *string `header:"style=simple,explode=false,name=x-goog-user-project"` +} + +func (g Globals) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(g, "", false) +} + +func (g *Globals) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { + return err + } + return nil +} + +func (g *Globals) GetAPIVersion() *string { + if g == nil { + return nil + } + return g.APIVersion +} + +func (g *Globals) GetAPIRevision() *string { + if g == nil { + return nil + } + return g.APIRevision +} + +func (g *Globals) GetUserProject() *string { + if g == nil { + return nil + } + return g.UserProject +} diff --git a/internal/sdk/sdkinternal/hooks/hooks.go b/internal/sdk/sdkinternal/hooks/hooks.go new file mode 100644 index 0000000..5e60c52 --- /dev/null +++ b/internal/sdk/sdkinternal/hooks/hooks.go @@ -0,0 +1,168 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package hooks + +import ( + "context" + "errors" + "net/http" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" +) + +type FailEarly struct { + Cause error +} + +var _ error = (*FailEarly)(nil) + +func (f *FailEarly) Error() string { + return f.Cause.Error() +} + +// HTTPClient provides an interface for supplying the SDK with a custom HTTP client +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +type HookContext struct { + SDK any + SDKConfiguration config.SDKConfiguration + BaseURL string + Context context.Context + OperationID string + OAuth2Scopes []string + SecuritySource func(context.Context) (interface{}, error) +} + +type BeforeRequestContext struct { + HookContext +} + +type AfterSuccessContext struct { + HookContext +} + +type AfterErrorContext struct { + HookContext +} + +// sdkInitHook is called when the SDK is initializing. The hook can modify and return a new baseURL and HTTP client to be used by the SDK. +type sdkInitHook interface { + SDKInit(config config.SDKConfiguration) config.SDKConfiguration +} + +// beforeRequestHook is called before the SDK sends a request. The hook can modify the request before it is sent or return an error to stop the request from being sent. +type beforeRequestHook interface { + BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) +} + +// afterSuccessHook is called after the SDK receives a response. The hook can modify the response before it is handled or return an error to stop the response from being handled. +type afterSuccessHook interface { + AfterSuccess(hookCtx AfterSuccessContext, res *http.Response) (*http.Response, error) +} + +// afterErrorHook is called after the SDK encounters an error, or a non-successful response. The hook can modify the response if available otherwise modify the error. +// All afterErrorHook hooks are called and returning an error won't stop the other hooks from being called. But if you want to stop the other hooks from being called, you can return a FailEarly error wrapping your error. +type afterErrorHook interface { + AfterError(hookCtx AfterErrorContext, res *http.Response, err error) (*http.Response, error) +} + +type Hooks struct { + sdkInitHooks []sdkInitHook + beforeRequestHook []beforeRequestHook + afterSuccessHook []afterSuccessHook + afterErrorHook []afterErrorHook +} + +var _ sdkInitHook = (*Hooks)(nil) +var _ beforeRequestHook = (*Hooks)(nil) +var _ afterSuccessHook = (*Hooks)(nil) +var _ afterErrorHook = (*Hooks)(nil) + +func New() *Hooks { + h := &Hooks{ + sdkInitHooks: []sdkInitHook{}, + beforeRequestHook: []beforeRequestHook{}, + afterSuccessHook: []afterSuccessHook{}, + afterErrorHook: []afterErrorHook{}, + } + + initHooks(h) + + return h +} + +// registerSDKInitHook registers a hook to be used by the SDK for the initialization event. +func (h *Hooks) registerSDKInitHook(hook sdkInitHook) { + h.sdkInitHooks = append(h.sdkInitHooks, hook) +} + +// registerBeforeRequestHook registers a hook to be used by the SDK for the before request event. +func (h *Hooks) registerBeforeRequestHook(hook beforeRequestHook) { + h.beforeRequestHook = append(h.beforeRequestHook, hook) +} + +// registerAfterSuccessHook registers a hook to be used by the SDK for the after success event. +func (h *Hooks) registerAfterSuccessHook(hook afterSuccessHook) { + h.afterSuccessHook = append(h.afterSuccessHook, hook) +} + +// registerAfterErrorHook registers a hook to be used by the SDK for the after error event. +func (h *Hooks) registerAfterErrorHook(hook afterErrorHook) { + h.afterErrorHook = append(h.afterErrorHook, hook) +} + +func (h *Hooks) SDKInit(config config.SDKConfiguration) config.SDKConfiguration { + for _, hook := range h.sdkInitHooks { + config = hook.SDKInit(config) + } + return config +} + +func (h *Hooks) BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) { + for _, hook := range h.beforeRequestHook { + var err error + req, err = hook.BeforeRequest(hookCtx, req) + if err != nil { + return req, err + } + } + return req, nil +} + +func (h *Hooks) AfterSuccess(hookCtx AfterSuccessContext, res *http.Response) (*http.Response, error) { + for _, hook := range h.afterSuccessHook { + var err error + res, err = hook.AfterSuccess(hookCtx, res) + if err != nil { + return res, err + } + } + return res, nil +} + +func (h *Hooks) AfterError(hookCtx AfterErrorContext, res *http.Response, err error) (*http.Response, error) { + for _, hook := range h.afterErrorHook { + res, err = hook.AfterError(hookCtx, res, err) + var fe *FailEarly + if errors.As(err, &fe) { + return nil, fe.Cause + } + } + return res, err +} diff --git a/internal/sdk/sdkinternal/hooks/interaction_errors.go b/internal/sdk/sdkinternal/hooks/interaction_errors.go new file mode 100644 index 0000000..7a9b095 --- /dev/null +++ b/internal/sdk/sdkinternal/hooks/interaction_errors.go @@ -0,0 +1,94 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hooks + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "mime" + "net/http" +) + +type interactionErrorHook struct{} + +func (h *interactionErrorHook) AfterSuccess(hookCtx AfterSuccessContext, res *http.Response) (*http.Response, error) { + return normalizeSingletonInteractionError(hookCtx.OperationID, res, nil) +} + +func (h *interactionErrorHook) AfterError(hookCtx AfterErrorContext, res *http.Response, err error) (*http.Response, error) { + originalRes := res + res, normalizedErr := normalizeSingletonInteractionError(hookCtx.OperationID, res, err) + var apiErr *apiHTTPError + if originalRes != nil && res == nil && errors.As(normalizedErr, &apiErr) && apiErr.RawResponse == originalRes { + return nil, &FailEarly{Cause: apiErr} + } + return res, normalizedErr +} + +func normalizeSingletonInteractionError(operationID string, res *http.Response, err error) (*http.Response, error) { + if !isInteractionOperation(operationID) || res == nil || res.StatusCode < http.StatusBadRequest || res.Body == nil || !isJSONContentType(res.Header.Get("Content-Type")) { + return res, err + } + + body, readErr := io.ReadAll(res.Body) + closeErr := res.Body.Close() + res.Body = io.NopCloser(bytes.NewReader(body)) + if readErr != nil { + if err != nil { + return res, err + } + return res, readErr + } + if closeErr != nil { + if err != nil { + return res, err + } + return res, closeErr + } + + var envelope []json.RawMessage + if json.Unmarshal(body, &envelope) != nil || len(envelope) != 1 { + return res, err + } + + var payload struct { + Error json.RawMessage `json:"error"` + } + if json.Unmarshal(envelope[0], &payload) != nil || len(payload.Error) == 0 || bytes.Equal(payload.Error, []byte("null")) { + return res, err + } + + return nil, &apiHTTPError{ + StatusCode: res.StatusCode, + Body: string(body), + RawResponse: res, + } +} + +func isInteractionOperation(operationID string) bool { + switch operationID { + case "CreateInteraction", "getInteractionById", "deleteInteraction", "cancelInteractionById": + return true + default: + return false + } +} + +func isJSONContentType(contentType string) bool { + mediaType, _, err := mime.ParseMediaType(contentType) + return err == nil && mediaType == "application/json" +} diff --git a/internal/sdk/sdkinternal/hooks/interaction_errors_test.go b/internal/sdk/sdkinternal/hooks/interaction_errors_test.go new file mode 100644 index 0000000..609dc3b --- /dev/null +++ b/internal/sdk/sdkinternal/hooks/interaction_errors_test.go @@ -0,0 +1,289 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hooks + +import ( + "errors" + "io" + "net/http" + "strings" + "testing" +) + +type closeTrackingBody struct { + io.Reader + closed bool +} + +func (b *closeTrackingBody) Close() error { + b.closed = true + return nil +} + +type failingBody struct { + closed bool +} + +func (b *failingBody) Read([]byte) (int, error) { + return 0, errors.New("read body") +} + +func (b *failingBody) Close() error { + b.closed = true + return nil +} + +func TestNormalizeSingletonInteractionError(t *testing.T) { + const arrayBody = `[{"error":{"code":403,"message":"insufficient scope"}}]` + sentinel := errors.New("original error") + + tests := []struct { + name string + operationID string + statusCode int + contentType string + body string + wantHandled bool + }{ + { + name: "singleton interaction error array", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json; charset=utf-8", + body: arrayBody, + wantHandled: true, + }, + { + name: "get interaction operation", + operationID: "getInteractionById", + statusCode: http.StatusInternalServerError, + contentType: "application/json", + body: arrayBody, + wantHandled: true, + }, + { + name: "delete interaction operation", + operationID: "deleteInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: arrayBody, + wantHandled: true, + }, + { + name: "cancel interaction operation", + operationID: "cancelInteractionById", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: arrayBody, + wantHandled: true, + }, + { + name: "non-interaction operation", + operationID: "listAgents", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: arrayBody, + }, + { + name: "successful response", + operationID: "CreateInteraction", + statusCode: http.StatusOK, + contentType: "application/json", + body: arrayBody, + }, + { + name: "non-JSON response", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "text/plain", + body: arrayBody, + }, + { + name: "vendor JSON response", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/problem+json", + body: arrayBody, + }, + { + name: "object response", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: `{"error":{"message":"normal shape"}}`, + }, + { + name: "empty array", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: `[]`, + }, + { + name: "malformed JSON", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: `[`, + }, + { + name: "null error", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: `[{"error":null}]`, + }, + { + name: "multiple array entries", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: `[{"error":{"message":"first"}},{"error":{"message":"second"}}]`, + }, + { + name: "singleton array without error", + operationID: "CreateInteraction", + statusCode: http.StatusForbidden, + contentType: "application/json", + body: `[{"message":"not an error envelope"}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalBody := &closeTrackingBody{Reader: strings.NewReader(tt.body)} + res := &http.Response{ + StatusCode: tt.statusCode, + Header: http.Header{"Content-Type": []string{tt.contentType}}, + Body: originalBody, + } + + gotRes, gotErr := normalizeSingletonInteractionError(tt.operationID, res, sentinel) + if tt.wantHandled { + if gotRes != nil { + t.Fatalf("response = %#v, want nil", gotRes) + } + var apiErr *apiHTTPError + if !errors.As(gotErr, &apiErr) { + t.Fatalf("error = %T %v, want *apiHTTPError", gotErr, gotErr) + } + if apiErr.StatusCode != tt.statusCode { + t.Errorf("status code = %d, want %d", apiErr.StatusCode, tt.statusCode) + } + if apiErr.Body != tt.body { + t.Errorf("error body = %q, want %q", apiErr.Body, tt.body) + } + if apiErr.RawResponse != res { + t.Error("raw response was not preserved") + } + } else { + if gotRes != res { + t.Error("response identity changed") + } + if !errors.Is(gotErr, sentinel) { + t.Errorf("error = %v, want original error", gotErr) + } + } + + wantClosed := isInteractionOperation(tt.operationID) && tt.statusCode >= http.StatusBadRequest && isJSONContentType(tt.contentType) + if originalBody.closed != wantClosed { + t.Errorf("original body closed = %t, want %t", originalBody.closed, wantClosed) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if string(body) != tt.body { + t.Errorf("response body = %q, want %q", body, tt.body) + } + }) + } +} + +func TestNormalizeSingletonInteractionErrorMissingResponseBody(t *testing.T) { + sentinel := errors.New("original error") + for _, res := range []*http.Response{ + nil, + {StatusCode: http.StatusForbidden, Header: http.Header{"Content-Type": []string{"application/json"}}}, + } { + gotRes, err := normalizeSingletonInteractionError("CreateInteraction", res, sentinel) + if gotRes != res { + t.Fatalf("response = %#v, want %#v", gotRes, res) + } + if !errors.Is(err, sentinel) { + t.Fatalf("error = %v, want original error", err) + } + } +} + +func TestNormalizeSingletonInteractionErrorPreservesIncomingErrorOnReadFailure(t *testing.T) { + sentinel := errors.New("original error") + body := &failingBody{} + res := &http.Response{ + StatusCode: http.StatusForbidden, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: body, + } + + gotRes, err := normalizeSingletonInteractionError("CreateInteraction", res, sentinel) + if gotRes != res { + t.Fatalf("response = %#v, want original response", gotRes) + } + if !errors.Is(err, sentinel) { + t.Fatalf("error = %v, want original error", err) + } + if !body.closed { + t.Fatal("response body was not closed") + } +} + +func TestInteractionErrorHookLifecycleMethods(t *testing.T) { + const body = `[{"error":{"message":"insufficient scope"}}]` + newResponse := func() *http.Response { + return &http.Response{ + StatusCode: http.StatusForbidden, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), + } + } + hook := &interactionErrorHook{} + + t.Run("after success", func(t *testing.T) { + res, err := hook.AfterSuccess(AfterSuccessContext{HookContext: HookContext{OperationID: "CreateInteraction"}}, newResponse()) + if res != nil { + t.Error("response is non-nil") + } + var apiErr *apiHTTPError + if !errors.As(err, &apiErr) { + t.Fatalf("error = %T %v, want *apiHTTPError", err, err) + } + }) + + t.Run("after error", func(t *testing.T) { + res, err := hook.AfterError(AfterErrorContext{HookContext: HookContext{OperationID: "CreateInteraction"}}, newResponse(), errors.New("original error")) + if res != nil { + t.Error("response is non-nil") + } + var failEarly *FailEarly + if !errors.As(err, &failEarly) { + t.Fatalf("error = %T %v, want *FailEarly", err, err) + } + var apiErr *apiHTTPError + if !errors.As(failEarly.Cause, &apiErr) { + t.Fatalf("cause = %T %v, want *apiHTTPError", failEarly.Cause, failEarly.Cause) + } + }) +} diff --git a/internal/sdk/sdkinternal/hooks/registration.go b/internal/sdk/sdkinternal/hooks/registration.go new file mode 100644 index 0000000..6e25b77 --- /dev/null +++ b/internal/sdk/sdkinternal/hooks/registration.go @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hooks + +/* + * This file is only ever generated once on the first generation and then is free to be modified. + * Any hooks you wish to add should be registered in the initHooks function. Feel free to define + * your hooks in this file or in separate files in the hooks package. + * + * Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance. + */ + +func initHooks(h *Hooks) { + h.registerSDKInitHook(userAgentHook{}) + + contractHook := &requestContractHook{} + h.registerBeforeRequestHook(contractHook) + h.registerAfterSuccessHook(contractHook) + + interactionErrors := &interactionErrorHook{} + h.registerAfterSuccessHook(interactionErrors) + h.registerAfterErrorHook(interactionErrors) + + // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance. + // Add any hooks you wish to add here. Feel free to define your hooks in this file or in + // separate files in the hooks package. + // + // The following methods are available for registering hooks: + _ = h.registerSDKInitHook + _ = h.registerBeforeRequestHook + _ = h.registerAfterSuccessHook + _ = h.registerAfterErrorHook + + // Example: + // exampleHook := &ExampleHook{} + // h.registerSDKInitHook(exampleHook) + // h.registerBeforeRequestHook(exampleHook) + // h.registerAfterErrorHook(exampleHook) + // h.registerAfterSuccessHook(exampleHook) +} diff --git a/internal/sdk/sdkinternal/hooks/request_contract.go b/internal/sdk/sdkinternal/hooks/request_contract.go new file mode 100644 index 0000000..f6d0724 --- /dev/null +++ b/internal/sdk/sdkinternal/hooks/request_contract.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hooks + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" +) + +type apiHTTPError struct { + StatusCode int + Body string + RawResponse *http.Response +} + +func (e *apiHTTPError) Error() string { + return fmt.Sprintf("API error: Status %d\n%s", e.StatusCode, e.Body) +} + +var apiVersionPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) + +type requestContractHook struct{} + +func (h *requestContractHook) BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) { + apiVersion := hookCtx.SDKConfiguration.Globals.APIVersion + // The pattern alone would admit the traversal segments "." and "..". + if apiVersion == nil || !apiVersionPattern.MatchString(*apiVersion) || *apiVersion == "." || *apiVersion == ".." { + return nil, fmt.Errorf("API version must be non-empty, contain only letters, numbers, '.', '_', or '-', and cannot be '.' or '..'; set --api-version, GEMINI_API_VERSION, or globals.api_version in the config file") + } + if userProject := hookCtx.SDKConfiguration.Globals.UserProject; userProject != nil && *userProject != "" { + req.Header.Set("x-goog-user-project", *userProject) + } + if req.Header.Get("Authorization") != "" { + req.Header.Del("x-goog-api-key") + } + if isStreamingInteraction(hookCtx.OperationID, req) { + req.Header.Set("Accept", "text/event-stream") + } + return req, nil +} + +func isStreamingInteraction(operationID string, req *http.Request) bool { + switch operationID { + case "getInteractionById": + return req.URL.Query().Get("stream") == "true" + case "CreateInteraction": + if req.GetBody == nil { + return false + } + body, err := req.GetBody() + if err != nil { + return false + } + defer body.Close() + var payload struct { + Stream bool `json:"stream"` + } + return json.NewDecoder(body).Decode(&payload) == nil && payload.Stream + default: + return false + } +} + +func (h *requestContractHook) AfterSuccess(_ AfterSuccessContext, res *http.Response) (*http.Response, error) { + if res == nil { + return nil, nil + } + + isSuccess := res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices + isGeneratedError := res.StatusCode >= http.StatusBadRequest + if isSuccess || isGeneratedError { + return res, nil + } + if res.Body == nil { + return nil, &apiHTTPError{StatusCode: res.StatusCode, RawResponse: res} + } + + body, readErr := io.ReadAll(res.Body) + _ = res.Body.Close() + if readErr != nil { + return res, readErr + } + res.Body = io.NopCloser(bytes.NewReader(body)) + + return nil, &apiHTTPError{ + StatusCode: res.StatusCode, + Body: string(body), + RawResponse: res, + } +} diff --git a/internal/sdk/sdkinternal/hooks/useragent.go b/internal/sdk/sdkinternal/hooks/useragent.go new file mode 100644 index 0000000..c9a9777 --- /dev/null +++ b/internal/sdk/sdkinternal/hooks/useragent.go @@ -0,0 +1,32 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hooks + +import ( + "fmt" + "runtime" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" +) + +// userAgentHook rewrites the default Speakeasy User-Agent into the +// google-genai client convention: {product}/{version} gl-{lang}/{runtime}, +// e.g. "google-genai-cli/0.5.4 gl-go/go1.24.0". +type userAgentHook struct{} + +func (userAgentHook) SDKInit(cfg config.SDKConfiguration) config.SDKConfiguration { + cfg.UserAgent = fmt.Sprintf("google-genai-cli/%s gl-go/%s", cfg.SDKVersion, runtime.Version()) + return cfg +} diff --git a/internal/sdk/sdkinternal/utils/contenttype.go b/internal/sdk/sdkinternal/utils/contenttype.go new file mode 100644 index 0000000..f075c13 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/contenttype.go @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "fmt" + "mime" + "strings" +) + +func MatchContentType(contentType string, pattern string) bool { + if contentType == "" { + contentType = "application/octet-stream" + } + + if contentType == pattern || pattern == "*" || pattern == "*/*" { + return true + } + + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + return false + } + + if mediaType == pattern { + return true + } + + parts := strings.Split(mediaType, "/") + if len(parts) == 2 { + if fmt.Sprintf("%s/*", parts[0]) == pattern || fmt.Sprintf("*/%s", parts[1]) == pattern { + return true + } + } + + return false +} diff --git a/src/lib/errors.ts b/internal/sdk/sdkinternal/utils/env.go similarity index 55% rename from src/lib/errors.ts rename to internal/sdk/sdkinternal/utils/env.go index 8244b56..fc21415 100644 --- a/src/lib/errors.ts +++ b/internal/sdk/sdkinternal/utils/env.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -12,23 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -export class CLIError extends Error { - constructor(message: string) { - super(message); - this.name = "CLIError"; - } -} +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. -export class APIError extends Error { - constructor(message: string) { - super(message); - this.name = "APIError"; - } -} +package utils + +import ( + "os" +) -export class ConfigError extends Error { - constructor(message: string) { - super(message); - this.name = "ConfigError"; - } +// GetEnv returns the value of the environment variable named by the key or the defaultValue if the environment variable is not set. +func GetEnv(name, defaultValue string) string { + value := os.Getenv(name) + if value == "" { + return defaultValue + } + return value } diff --git a/internal/sdk/sdkinternal/utils/form.go b/internal/sdk/sdkinternal/utils/form.go new file mode 100644 index 0000000..88db782 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/form.go @@ -0,0 +1,175 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "fmt" + "math/big" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/optionalnullable" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +func populateForm(paramName string, explode bool, objType reflect.Type, objValue reflect.Value, delimiter string, defaultValue *string, allowEmptyValue map[string]struct{}, getFieldName func(reflect.StructField) string) url.Values { + + formValues := url.Values{} + + if isNil(objType, objValue) { + if defaultValue != nil { + formValues.Add(paramName, *defaultValue) + } else if _, ok := allowEmptyValue[paramName]; ok { + formValues.Add(paramName, "") + } + + return formValues + } + + if objType.Kind() == reflect.Pointer { + objType = objType.Elem() + objValue = objValue.Elem() + } + + switch objType.Kind() { + case reflect.Struct: + switch objValue.Interface().(type) { + case time.Time: + formValues.Add(paramName, valToString(objValue.Interface())) + case types.Date: + formValues.Add(paramName, valToString(objValue.Interface())) + case big.Int: + formValues.Add(paramName, valToString(objValue.Interface())) + default: + var items []string + + for i := 0; i < objType.NumField(); i++ { + fieldType := objType.Field(i) + valType := objValue.Field(i) + + if isNil(fieldType.Type, valType) { + continue + } + + if valType.Kind() == reflect.Pointer { + valType = valType.Elem() + } + + valType, hasValue := unwrapOptionalNullable(valType) + if !hasValue { + continue + } + + fieldName := getFieldName(fieldType) + if fieldName == "" { + continue + } + + if explode { + if valType.Kind() == reflect.Slice || valType.Kind() == reflect.Array { + for i := 0; i < valType.Len(); i++ { + formValues.Add(fieldName, valToString(valType.Index(i).Interface())) + } + } else { + formValues.Add(fieldName, valToString(valType.Interface())) + } + } else { + items = append(items, fmt.Sprintf("%s%s%s", fieldName, delimiter, valToString(valType.Interface()))) + } + } + + if len(items) > 0 { + formValues.Add(paramName, strings.Join(items, delimiter)) + } + } + case reflect.Map: + // check if optionalnullable.OptionalNullable[T] + if nullableValue, ok := optionalnullable.AsOptionalNullable(objValue); ok { + // Serialize the wrapped value using the rules for its own type + if value, isSet := nullableValue.GetUntyped(); isSet && value != nil { + innerValue := reflect.ValueOf(value) + return populateForm(paramName, explode, innerValue.Type(), innerValue, delimiter, defaultValue, allowEmptyValue, getFieldName) + } + // If not set or explicitly null, skip adding to form + return formValues + } + + items := []string{} + + iter := objValue.MapRange() + for iter.Next() { + if explode { + formValues.Add(iter.Key().String(), valToString(iter.Value().Interface())) + } else { + items = append(items, fmt.Sprintf("%s%s%s", iter.Key().String(), delimiter, valToString(iter.Value().Interface()))) + } + } + + if len(items) > 0 { + formValues.Add(paramName, strings.Join(items, delimiter)) + } + case reflect.Slice, reflect.Array: + if objValue.Len() == 0 { + if _, ok := allowEmptyValue[paramName]; ok { + formValues.Add(paramName, "") + } + } else { + values := parseDelimitedArray(explode, objValue, delimiter) + for _, v := range values { + formValues.Add(paramName, v) + } + } + default: + // For string types, use the value directly without conversion + if objType.Kind() == reflect.String { + stringValue := objValue.String() + formValues.Add(paramName, stringValue) + } else { + stringValue := valToString(objValue.Interface()) + if stringValue == "" { + if _, ok := allowEmptyValue[paramName]; ok { + formValues.Add(paramName, "") + } + } else if stringValue != "" { + formValues.Add(paramName, stringValue) + } + } + } + + return formValues +} + +func parseDelimitedArray(explode bool, objValue reflect.Value, delimiter string) []string { + values := []string{} + items := []string{} + + for i := 0; i < objValue.Len(); i++ { + if explode { + values = append(values, valToString(objValue.Index(i).Interface())) + } else { + items = append(items, valToString(objValue.Index(i).Interface())) + } + } + + if len(items) > 0 { + values = append(values, strings.Join(items, delimiter)) + } + + return values +} diff --git a/internal/sdk/sdkinternal/utils/headers.go b/internal/sdk/sdkinternal/utils/headers.go new file mode 100644 index 0000000..d5b8d24 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/headers.go @@ -0,0 +1,195 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "context" + "fmt" + "math/big" + "net/http" + "reflect" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/optionalnullable" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +func PopulateHeaders(_ context.Context, req *http.Request, headers interface{}, globals interface{}) { + globalsAlreadyPopulated := populateHeaders(headers, globals, req.Header, []string{}) + if globals != nil { + _ = populateHeaders(globals, nil, req.Header, globalsAlreadyPopulated) + } +} + +func populateHeaders(headers interface{}, globals interface{}, reqHeaders http.Header, skipFields []string) []string { + headerParamsStructType, headerParamsValType := dereferencePointers(reflect.TypeOf(headers), reflect.ValueOf(headers)) + + globalsAlreadyPopulated := []string{} + + for i := 0; i < headerParamsStructType.NumField(); i++ { + fieldType := headerParamsStructType.Field(i) + valType := headerParamsValType.Field(i) + + if contains(skipFields, fieldType.Name) { + continue + } + + if globals != nil { + var globalFound bool + fieldType, valType, globalFound = populateFromGlobals(fieldType, valType, headerParamTagKey, globals) + if globalFound { + globalsAlreadyPopulated = append(globalsAlreadyPopulated, fieldType.Name) + } + } + + tag := parseParamTag(headerParamTagKey, fieldType, "simple", false) + if tag == nil { + continue + } + + defaultConstValue := handleDefaultConstHeaderValue(valType, fieldType.Tag) + if defaultConstValue != "" { + reqHeaders.Add(tag.ParamName, defaultConstValue) + continue + } + + value := serializeHeader(fieldType.Type, valType, tag.Explode) + if value != "" { + reqHeaders.Add(tag.ParamName, value) + } + } + + return globalsAlreadyPopulated +} + +func handleDefaultConstHeaderValue(v reflect.Value, tag reflect.StructTag) string { + constTag := tag.Get("const") + if constTag != "" { + return constTag + } + + if isNil(v.Type(), v) { + defaultTag := tag.Get("default") + if defaultTag != "" { + return defaultTag + } + } + + return "" +} + +func serializeHeader(objType reflect.Type, objValue reflect.Value, explode bool) string { + if isNil(objType, objValue) { + return "" + } + + if objType.Kind() == reflect.Pointer { + objType = objType.Elem() + objValue = objValue.Elem() + } + + switch objType.Kind() { + case reflect.Struct: + switch objValue.Interface().(type) { + case time.Time, types.Date, big.Int: + return valToString(objValue.Interface()) + } + + items := []string{} + + for i := 0; i < objType.NumField(); i++ { + fieldType := objType.Field(i) + valType := objValue.Field(i) + + if isNil(fieldType.Type, valType) { + continue + } + + if fieldType.Type.Kind() == reflect.Pointer { + valType = valType.Elem() + } + + valType, hasValue := unwrapOptionalNullable(valType) + if !hasValue { + continue + } + + tag := parseParamTag(headerParamTagKey, fieldType, "simple", false) + if tag == nil { + continue + } + + fieldName := tag.ParamName + + if fieldName == "" { + continue + } + + var value string + + defaultConstValue := handleDefaultConstHeaderValue(valType, fieldType.Tag) + if defaultConstValue != "" { + value = defaultConstValue + } else { + value = valToString(valType.Interface()) + } + + if explode { + items = append(items, fmt.Sprintf("%s=%s", fieldName, value)) + } else { + items = append(items, fieldName, value) + } + } + + return strings.Join(items, ",") + case reflect.Map: + // check if optionalnullable.OptionalNullable[T] + if nullableValue, ok := optionalnullable.AsOptionalNullable(objValue); ok { + // Serialize the wrapped value using the rules for its own type + if value, isSet := nullableValue.GetUntyped(); isSet && value != nil { + innerValue := reflect.ValueOf(value) + return serializeHeader(innerValue.Type(), innerValue, explode) + } + // If not set or explicitly null, return empty string + return "" + } + + items := []string{} + + iter := objValue.MapRange() + for iter.Next() { + if explode { + items = append(items, fmt.Sprintf("%s=%s", iter.Key().String(), valToString(iter.Value().Interface()))) + } else { + items = append(items, iter.Key().String(), valToString(iter.Value().Interface())) + } + } + + return strings.Join(items, ",") + case reflect.Slice, reflect.Array: + items := []string{} + + for i := 0; i < objValue.Len(); i++ { + items = append(items, valToString(objValue.Index(i).Interface())) + } + + return strings.Join(items, ",") + default: + return valToString(objValue.Interface()) + } +} diff --git a/internal/sdk/sdkinternal/utils/json.go b/internal/sdk/sdkinternal/utils/json.go new file mode 100644 index 0000000..d5a4eb6 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/json.go @@ -0,0 +1,779 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strconv" + "strings" + "time" + "unsafe" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +func MarshalJSON(v interface{}, tag reflect.StructTag, topLevel bool) ([]byte, error) { + // Handle nil interface early + if v == nil { + return []byte("null"), nil + } + + // Check for nil pointer before dereferencing to avoid creating invalid reflect.Value + origVal := reflect.ValueOf(v) + if origVal.Kind() == reflect.Ptr && origVal.IsNil() { + return []byte("null"), nil + } + + typ, val := dereferencePointers(reflect.TypeOf(v), origVal) + + switch { + case isModelType(typ): + // When topLevel=true, only use json.Marshal if the type has a custom MarshalJSON + // to ensure nested structs with custom tags (like integer:"string") are handled correctly + if topLevel && implementsJSONMarshaler(v) { + return json.Marshal(v) + } + + if isNil(typ, val) || !val.IsValid() { + return []byte("null"), nil + } + + out := map[string]json.RawMessage{} + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldVal := val.Field(i) + + fieldName := field.Name + + omitEmpty := false + omitZero := false + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + for _, tag := range strings.Split(jsonTag, ",") { + if tag == "omitempty" { + omitEmpty = true + } else if tag == "omitzero" { + omitZero = true + } else { + fieldName = tag + } + } + } + + if (omitEmpty || omitZero) && field.Tag.Get("const") == "" { + // Both omitempty and omitzero skip zero values (including nil) + if isNil(field.Type, fieldVal) { + continue + } + + if omitZero && fieldVal.IsZero() { + continue + } + + if omitEmpty && fieldVal.Kind() != reflect.Struct && fieldVal.IsZero() { + continue + } + + if omitEmpty && isEmptyContainer(field.Type, fieldVal) { + continue + } + } + + if !field.IsExported() && field.Tag.Get("const") == "" { + continue + } + + additionalProperties := field.Tag.Get("additionalProperties") + if fieldName == "-" && additionalProperties == "" { + continue + } + + if additionalProperties == "true" { + if isNil(field.Type, fieldVal) { + continue + } + fieldVal := trueReflectValue(fieldVal) + if fieldVal.Type().Kind() != reflect.Map { + return nil, fmt.Errorf("additionalProperties must be a map") + } + + for _, key := range fieldVal.MapKeys() { + r, err := marshalValue(fieldVal.MapIndex(key).Interface(), field.Tag) + if err != nil { + return nil, err + } + + out[key.String()] = r + } + + continue + } + + var fv interface{} + + if field.IsExported() { + fv = fieldVal.Interface() + } else { + pt := reflect.New(typ).Elem() + pt.Set(val) + + pf := pt.Field(i) + + fv = reflect.NewAt(pf.Type(), unsafe.Pointer(pf.UnsafeAddr())).Elem().Interface() + } + + r, err := marshalValue(fv, field.Tag) + if err != nil { + return nil, err + } + + out[fieldName] = r + } + + return json.Marshal(out) + default: + return marshalValue(v, tag) + } +} + +func UnmarshalJSON(b []byte, v interface{}, tag reflect.StructTag, topLevel bool, requiredFields []string) error { + if reflect.TypeOf(v).Kind() != reflect.Ptr { + return errors.New("v must be a pointer") + } + + typ, val := dereferencePointers(reflect.TypeOf(v), reflect.ValueOf(v)) + + switch { + case isModelType(typ): + if bytes.Equal(b, []byte("null")) { + return json.Unmarshal(b, v) + } + // When topLevel=true, only use json.Unmarshal if the type has a custom UnmarshalJSON + // to ensure nested structs with custom tags (like integer:"string") are handled correctly + if topLevel && implementsJSONUnmarshaler(reflect.TypeOf(v)) { + return json.Unmarshal(b, v) + } + + var unmarshaled map[string]json.RawMessage + + if err := json.Unmarshal(b, &unmarshaled); err != nil { + return err + } + + missingFields := []string{} + for _, requiredField := range requiredFields { + if _, ok := unmarshaled[requiredField]; !ok { + missingFields = append(missingFields, requiredField) + } + } + if len(missingFields) > 0 { + return fmt.Errorf("missing required fields: %s", strings.Join(missingFields, ", ")) + } + + var additionalPropertiesField *reflect.StructField + var additionalPropertiesValue *reflect.Value + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldVal := val.Field(i) + + fieldName := field.Name + + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + for _, tag := range strings.Split(jsonTag, ",") { + if tag != "omitempty" && tag != "omitzero" { + fieldName = tag + } + } + } + + if field.Tag.Get("additionalProperties") == "true" { + additionalPropertiesField = &field + additionalPropertiesValue = &fieldVal + continue + } + + // If we receive a value for a const field ignore it but mark it as unmarshaled + if field.Tag.Get("const") != "" { + if r, ok := unmarshaled[fieldName]; ok { + val := string(r) + + if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) { + var err error + val, err = strconv.Unquote(val) + if err != nil { + return fmt.Errorf("failed to unquote const field `%s` value `%s`: %w", fieldName, val, err) + } + } + constValue := field.Tag.Get("const") + if val != constValue { + return fmt.Errorf("const field `%s` does not match expected value `%s` got `%s`", fieldName, constValue, val) + } + + delete(unmarshaled, fieldName) + } + } else if !field.IsExported() { + continue + } + + value, ok := unmarshaled[fieldName] + if !ok { + defaultTag, defaultOk := field.Tag.Lookup("default") + if defaultOk { + value = handleDefaultConstValue(defaultTag, fieldVal.Interface(), field.Tag) + ok = true + } + } else { + delete(unmarshaled, fieldName) + } + + if ok { + if err := unmarshalValue(value, fieldVal, field.Tag); err != nil { + return err + } + } + } + + keys := make([]string, 0, len(unmarshaled)) + for k := range unmarshaled { + keys = append(keys, k) + } + + if len(keys) > 0 { + if additionalPropertiesField != nil && additionalPropertiesValue != nil { + typeOfMap := additionalPropertiesField.Type + if additionalPropertiesValue.Type().Kind() == reflect.Interface { + typeOfMap = reflect.TypeOf(map[string]interface{}{}) + } else if additionalPropertiesValue.Type().Kind() != reflect.Map { + return fmt.Errorf("additionalProperties must be a map") + } + + mapValue := reflect.MakeMap(typeOfMap) + + for key, value := range unmarshaled { + val := reflect.New(typeOfMap.Elem()) + + if err := unmarshalValue(value, val, additionalPropertiesField.Tag); err != nil { + return err + } + + if val.Elem().Type().String() == typeOfMap.Elem().String() { + mapValue.SetMapIndex(reflect.ValueOf(key), val.Elem()) + } else { + mapValue.SetMapIndex(reflect.ValueOf(key), trueReflectValue(val)) + } + + } + if additionalPropertiesValue.Type().Kind() == reflect.Interface { + additionalPropertiesValue.Set(mapValue) + } else { + additionalPropertiesValue.Set(mapValue) + } + } + } + default: + return unmarshalValue(b, reflect.ValueOf(v), tag) + } + + return nil +} + +func marshalValue(v interface{}, tag reflect.StructTag) (json.RawMessage, error) { + constTag := tag.Get("const") + if constTag != "" { + return handleDefaultConstValue(constTag, v, tag), nil + } + + if isNil(reflect.TypeOf(v), reflect.ValueOf(v)) { + defaultTag, ok := tag.Lookup("default") + if ok { + return handleDefaultConstValue(defaultTag, v, tag), nil + } + + return []byte("null"), nil + } + + typ, val := dereferencePointers(reflect.TypeOf(v), reflect.ValueOf(v)) + switch typ.Kind() { + case reflect.Int64: + format := tag.Get("integer") + if format == "string" { + b := val.Interface().(int64) + return []byte(fmt.Sprintf(`"%d"`, b)), nil + } + case reflect.Float64: + format := tag.Get("number") + if format == "string" { + b := val.Interface().(float64) + return []byte(fmt.Sprintf(`"%g"`, b)), nil + } + case reflect.Map: + if isNil(typ, val) { + return []byte("null"), nil + } + + // Check if the map implements json.Marshaler (like optionalnullable.OptionalNullable[T]) + if marshaler, ok := val.Interface().(json.Marshaler); ok { + return marshaler.MarshalJSON() + } + + out := map[string]json.RawMessage{} + + for _, key := range val.MapKeys() { + itemVal := val.MapIndex(key) + + if isNil(itemVal.Type(), itemVal) { + out[key.String()] = []byte("null") + continue + } + + r, err := marshalValue(itemVal.Interface(), tag) + if err != nil { + return nil, err + } + + out[key.String()] = r + } + + return json.Marshal(out) + case reflect.Slice, reflect.Array: + if isNil(typ, val) { + return []byte("null"), nil + } + + // []byte is special-cased by encoding/json to use base64 encoding. + // Delegate directly to avoid treating individual bytes as array elements. + if typ.Elem().Kind() == reflect.Uint8 { + return json.Marshal(val.Interface()) + } + + out := []json.RawMessage{} + + for i := 0; i < val.Len(); i++ { + itemVal := val.Index(i) + + if isNil(itemVal.Type(), itemVal) { + out = append(out, []byte("null")) + continue + } + + r, err := marshalValue(itemVal.Interface(), tag) + if err != nil { + return nil, err + } + + out = append(out, r) + } + + return json.Marshal(out) + case reflect.Struct: + switch typ { + case reflect.TypeOf(time.Time{}): + return []byte(fmt.Sprintf(`"%s"`, val.Interface().(time.Time).Format(time.RFC3339Nano))), nil + case reflect.TypeOf(big.Int{}): + format := tag.Get("bigint") + if format == "string" { + b := val.Interface().(big.Int) + return []byte(fmt.Sprintf(`"%s"`, (&b).String())), nil + } + default: + // For model types without custom MarshalJSON, use field processing + // to handle custom tags like integer:"string" + if isModelType(typ) && !implementsJSONMarshaler(v) { + return MarshalJSON(v, "", false) + } + } + } + + return json.Marshal(v) +} + +func implementsJSONMarshaler(v interface{}) bool { + marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem() + vType := reflect.TypeOf(v) + if vType.Implements(marshalerType) { + return true + } + if vType.Kind() == reflect.Ptr { + // For double pointers (e.g., **TypeA), check if the inner pointer type + // implements the interface (e.g., *TypeA) + if vType.Elem().Implements(marshalerType) { + return true + } + // Also check if pointer to element implements it + return reflect.PtrTo(vType.Elem()).Implements(marshalerType) + } + return reflect.PtrTo(vType).Implements(marshalerType) +} + +func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { + if tagValue == "null" { + return []byte("null") + } + + typ := dereferenceTypePointer(reflect.TypeOf(val)) + switch typ { + case reflect.TypeOf(time.Time{}): + return []byte(fmt.Sprintf(`"%s"`, tagValue)) + case reflect.TypeOf(big.Int{}): + bigIntTag := tag.Get("bigint") + if bigIntTag == "string" { + return []byte(fmt.Sprintf(`"%s"`, tagValue)) + } + case reflect.TypeOf(int64(0)): + format := tag.Get("integer") + if format == "string" { + return []byte(fmt.Sprintf(`"%s"`, tagValue)) + } + case reflect.TypeOf(float64(0)): + format := tag.Get("number") + if format == "string" { + return []byte(fmt.Sprintf(`"%s"`, tagValue)) + } + case reflect.TypeOf(types.Date{}): + return []byte(fmt.Sprintf(`"%s"`, tagValue)) + default: + if typ.Kind() == reflect.String { + return []byte(fmt.Sprintf("%q", tagValue)) + } + } + + return []byte(tagValue) +} + +func unmarshalValue(value json.RawMessage, v reflect.Value, tag reflect.StructTag) error { + if bytes.Equal(value, []byte("null")) { + if v.CanAddr() { + return json.Unmarshal(value, v.Addr().Interface()) + } else { + return json.Unmarshal(value, v.Interface()) + } + } + + typ := dereferenceTypePointer(v.Type()) + + switch typ.Kind() { + case reflect.Int64: + var b int64 + + format := tag.Get("integer") + if format == "string" { + var s string + if err := json.Unmarshal(value, &s); err != nil { + return err + } + + var err error + b, err = strconv.ParseInt(s, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse string as int64: %w", err) + } + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(typ)) + } + v = v.Elem() + } + + v.Set(reflect.ValueOf(b)) + return nil + } + case reflect.Float64: + var b float64 + + format := tag.Get("number") + if format == "string" { + var s string + if err := json.Unmarshal(value, &s); err != nil { + return err + } + + var err error + b, err = strconv.ParseFloat(s, 64) + if err != nil { + return fmt.Errorf("failed to parse string as float64: %w", err) + } + + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(typ)) + } + v = v.Elem() + } + + v.Set(reflect.ValueOf(b)) + return nil + } + case reflect.Map: + if implementsJSONUnmarshaler(v.Type()) { + if v.CanAddr() { + return json.Unmarshal(value, v.Addr().Interface()) + } + return json.Unmarshal(value, v.Interface()) + } + + if bytes.Equal(value, []byte("null")) || !isComplexValueType(dereferenceTypePointer(typ.Elem())) { + if v.CanAddr() { + return json.Unmarshal(value, v.Addr().Interface()) + } else { + return json.Unmarshal(value, v.Interface()) + } + } + + var unmarshaled map[string]json.RawMessage + + if err := json.Unmarshal(value, &unmarshaled); err != nil { + return err + } + + m := reflect.MakeMap(typ) + + for k, value := range unmarshaled { + itemVal := reflect.New(typ.Elem()) + + if err := unmarshalValue(value, itemVal, tag); err != nil { + return err + } + + m.SetMapIndex(reflect.ValueOf(k), itemVal.Elem()) + } + + // Dereference pointer before setting the map value. + // v may be a pointer to a map (e.g., from reflect.ValueOf(&mapVar)). + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + v.Set(m) + return nil + case reflect.Slice, reflect.Array: + // []byte is special-cased by encoding/json to use base64 encoding. + // Delegate directly to avoid treating the base64 string as a JSON array. + if typ.Elem().Kind() == reflect.Uint8 { + if v.CanAddr() { + return json.Unmarshal(value, v.Addr().Interface()) + } + return json.Unmarshal(value, v.Interface()) + } + + var unmarshaled []json.RawMessage + + if err := json.Unmarshal(value, &unmarshaled); err != nil { + return err + } + + arrVal := reflect.MakeSlice(typ, len(unmarshaled), len(unmarshaled)) + + for index, value := range unmarshaled { + itemVal := reflect.New(typ.Elem()) + + if err := unmarshalValue(value, itemVal, tag); err != nil { + return err + } + + arrVal.Index(index).Set(itemVal.Elem()) + } + + if v.Kind() == reflect.Pointer { + if v.IsNil() { + v.Set(reflect.New(typ)) + } + v = v.Elem() + } + + v.Set(arrVal) + return nil + case reflect.Struct: + switch typ { + case reflect.TypeOf(time.Time{}): + var s string + if err := json.Unmarshal(value, &s); err != nil { + return err + } + + t, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + return fmt.Errorf("failed to parse string as time.Time: %w", err) + } + + if v.Kind() == reflect.Ptr { + if v.IsNil() && v.CanSet() { + v.Set(reflect.New(typ)) + } + v = v.Elem() + } + + v.Set(reflect.ValueOf(t)) + return nil + case reflect.TypeOf(big.Int{}): + var b *big.Int + + format := tag.Get("bigint") + if format == "string" { + var s string + if err := json.Unmarshal(value, &s); err != nil { + return err + } + + var ok bool + b, ok = new(big.Int).SetString(s, 10) + if !ok { + return fmt.Errorf("failed to parse string as big.Int") + } + } else { + if err := json.Unmarshal(value, &b); err != nil { + return err + } + } + + if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Ptr { + v = v.Elem() + } + + v.Set(reflect.ValueOf(b)) + return nil + case reflect.TypeOf(types.Date{}): + var s string + + if err := json.Unmarshal(value, &s); err != nil { + return err + } + + d, err := types.DateFromString(s) + if err != nil { + return fmt.Errorf("failed to parse string as types.Date: %w", err) + } + + if v.Kind() == reflect.Ptr { + if v.IsNil() && v.CanSet() { + v.Set(reflect.New(typ)) + } + v = v.Elem() + } + + v.Set(reflect.ValueOf(d)) + return nil + default: + // For model types without custom UnmarshalJSON, use field processing + // to handle custom tags like integer:"string" + if isModelType(typ) && !implementsJSONUnmarshaler(v.Type()) { + // If v is already a pointer, we can unmarshal directly into it + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + // Handle double pointers (e.g., **Struct for nullable array elements) + inner := v.Elem() + if inner.Kind() == reflect.Ptr { + if inner.IsNil() { + inner.Set(reflect.New(typ)) + } + return UnmarshalJSON(value, inner.Interface(), "", false, nil) + } + return UnmarshalJSON(value, v.Interface(), "", false, nil) + } + // For non-pointer struct values that are addressable + if v.CanAddr() { + return UnmarshalJSON(value, v.Addr().Interface(), "", false, nil) + } + // For non-addressable struct values, fall through to json.Unmarshal + } + } + } + + var val interface{} + + if v.CanAddr() { + val = v.Addr().Interface() + } else { + val = v.Interface() + } + + return json.Unmarshal(value, val) +} + +func implementsJSONUnmarshaler(typ reflect.Type) bool { + unmarshalerType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() + if typ.Implements(unmarshalerType) { + return true + } + if typ.Kind() == reflect.Ptr { + // For double pointers (e.g., **TypeA), check if the inner pointer type + // implements the interface (e.g., *TypeA) + if typ.Elem().Implements(unmarshalerType) { + return true + } + // Also check if pointer to element implements it + return reflect.PtrTo(typ.Elem()).Implements(unmarshalerType) + } + return reflect.PtrTo(typ).Implements(unmarshalerType) +} + +func dereferencePointers(typ reflect.Type, val reflect.Value) (reflect.Type, reflect.Value) { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } else { + return typ, val + } + + return dereferencePointers(typ, val) +} + +func dereferenceTypePointer(typ reflect.Type) reflect.Type { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } else { + return typ + } + + return dereferenceTypePointer(typ) +} + +func isComplexValueType(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Struct: + switch typ { + case reflect.TypeOf(time.Time{}): + fallthrough + case reflect.TypeOf(big.Int{}): + fallthrough + case reflect.TypeOf(types.Date{}): + return true + } + } + + return false +} + +func isModelType(typ reflect.Type) bool { + if isComplexValueType(typ) { + return false + } + + if typ.Kind() == reflect.Struct { + return true + } + + return false +} diff --git a/internal/sdk/sdkinternal/utils/pathparams.go b/internal/sdk/sdkinternal/utils/pathparams.go new file mode 100644 index 0000000..e92226a --- /dev/null +++ b/internal/sdk/sdkinternal/utils/pathparams.go @@ -0,0 +1,199 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "context" + "fmt" + "math/big" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/optionalnullable" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +func GenerateURL(_ context.Context, serverURL, path string, pathParams interface{}, globals interface{}) (string, error) { + uri := strings.TrimSuffix(serverURL, "/") + path + + parsedParameters := map[string]string{} + + globalsAlreadyPopulated, err := populateParsedParameters(pathParams, globals, parsedParameters, []string{}) + if err != nil { + return "", err + } + + if globals != nil { + _, err = populateParsedParameters(globals, nil, parsedParameters, globalsAlreadyPopulated) + if err != nil { + return "", err + } + } + + // TODO should we handle the case where there are no matching path params? + return ReplaceParameters(uri, parsedParameters), nil +} + +func populateParsedParameters(pathParams interface{}, globals interface{}, parsedParameters map[string]string, skipFields []string) ([]string, error) { + pathParamsStructType, pathParamsValType := dereferencePointers(reflect.TypeOf(pathParams), reflect.ValueOf(pathParams)) + + globalsAlreadyPopulated := []string{} + + for i := 0; i < pathParamsStructType.NumField(); i++ { + fieldType := pathParamsStructType.Field(i) + valType := pathParamsValType.Field(i) + + if contains(skipFields, fieldType.Name) { + continue + } + + requestTag := getRequestTag(fieldType) + if requestTag != nil { + continue + } + + ppTag := parseParamTag(pathParamTagKey, fieldType, "simple", false) + if ppTag == nil { + continue + } + + if globals != nil { + var globalFound bool + fieldType, valType, globalFound = populateFromGlobals(fieldType, valType, pathParamTagKey, globals) + if globalFound { + globalsAlreadyPopulated = append(globalsAlreadyPopulated, fieldType.Name) + } + } + + if ppTag.Serialization != "" { + vals, err := populateSerializedParams(ppTag, fieldType.Type, valType) + if err != nil { + return nil, err + } + for k, v := range vals { + parsedParameters[k] = url.PathEscape(v) + } + } else { + // TODO: support other styles + switch ppTag.Style { + case "simple": + simpleParams := getSimplePathParams(ppTag.ParamName, fieldType.Type, valType, ppTag.Explode) + for k, v := range simpleParams { + parsedParameters[k] = v + } + } + } + } + + return globalsAlreadyPopulated, nil +} + +func getSimplePathParams(parentName string, objType reflect.Type, objValue reflect.Value, explode bool) map[string]string { + pathParams := make(map[string]string) + + if isNil(objType, objValue) { + return nil + } + + if objType.Kind() == reflect.Ptr { + objType = objType.Elem() + objValue = objValue.Elem() + } + + switch objType.Kind() { + case reflect.Array, reflect.Slice: + if objValue.Len() == 0 { + return nil + } + var ppVals []string + for i := 0; i < objValue.Len(); i++ { + ppVals = append(ppVals, valToString(objValue.Index(i).Interface())) + } + pathParams[parentName] = strings.Join(ppVals, ",") + case reflect.Map: + // check if optionalnullable.OptionalNullable[T] + if nullableValue, ok := optionalnullable.AsOptionalNullable(objValue); ok { + // Serialize the wrapped value using the rules for its own type + if value, isSet := nullableValue.GetUntyped(); isSet && value != nil { + innerValue := reflect.ValueOf(value) + return getSimplePathParams(parentName, innerValue.Type(), innerValue, explode) + } + // If not set or explicitly null, return nil (skip parameter) + return pathParams + } + + if objValue.Len() == 0 { + return nil + } + var ppVals []string + objMap := objValue.MapRange() + for objMap.Next() { + if explode { + ppVals = append(ppVals, fmt.Sprintf("%s=%s", objMap.Key().String(), valToString(objMap.Value().Interface()))) + } else { + ppVals = append(ppVals, fmt.Sprintf("%s,%s", objMap.Key().String(), valToString(objMap.Value().Interface()))) + } + } + pathParams[parentName] = strings.Join(ppVals, ",") + case reflect.Struct: + switch objValue.Interface().(type) { + case time.Time: + pathParams[parentName] = valToString(objValue.Interface()) + case types.Date: + pathParams[parentName] = valToString(objValue.Interface()) + case big.Int: + pathParams[parentName] = valToString(objValue.Interface()) + default: + var ppVals []string + for i := 0; i < objType.NumField(); i++ { + fieldType := objType.Field(i) + valType := objValue.Field(i) + + ppTag := parseParamTag(pathParamTagKey, fieldType, "simple", explode) + if ppTag == nil { + continue + } + + if isNil(fieldType.Type, valType) { + continue + } + + if fieldType.Type.Kind() == reflect.Pointer { + valType = valType.Elem() + } + + valType, hasValue := unwrapOptionalNullable(valType) + if !hasValue { + continue + } + + if explode { + ppVals = append(ppVals, fmt.Sprintf("%s=%s", ppTag.ParamName, valToString(valType.Interface()))) + } else { + ppVals = append(ppVals, fmt.Sprintf("%s,%s", ppTag.ParamName, valToString(valType.Interface()))) + } + } + pathParams[parentName] = strings.Join(ppVals, ",") + } + default: + pathParams[parentName] = valToString(objValue.Interface()) + } + + return pathParams +} diff --git a/internal/sdk/sdkinternal/utils/queryparams.go b/internal/sdk/sdkinternal/utils/queryparams.go new file mode 100644 index 0000000..e8d8c6e --- /dev/null +++ b/internal/sdk/sdkinternal/utils/queryparams.go @@ -0,0 +1,323 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "net/http" + "net/url" + "reflect" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/optionalnullable" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +func PopulateQueryParams(_ context.Context, req *http.Request, queryParams interface{}, globals interface{}, allowEmptyValue map[string]struct{}) error { + // Query parameters may already be present from overriding URL + if req.URL.RawQuery != "" { + return nil + } + + values := url.Values{} + + globalsAlreadyPopulated, err := populateQueryParams(queryParams, globals, values, []string{}, allowEmptyValue) + if err != nil { + return err + } + + if globals != nil { + _, err = populateQueryParams(globals, nil, values, globalsAlreadyPopulated, allowEmptyValue) + if err != nil { + return err + } + } + + req.URL.RawQuery = values.Encode() + + return nil +} + +func populateQueryParams(queryParams interface{}, globals interface{}, values url.Values, skipFields []string, allowEmptyValue map[string]struct{}) ([]string, error) { + queryParamsVal := reflect.ValueOf(queryParams) + if queryParamsVal.Kind() == reflect.Pointer && queryParamsVal.IsNil() { + return nil, nil + } + queryParamsStructType, queryParamsValType := dereferencePointers(reflect.TypeOf(queryParams), queryParamsVal) + + globalsAlreadyPopulated := []string{} + for i := 0; i < queryParamsStructType.NumField(); i++ { + fieldType := queryParamsStructType.Field(i) + valType := queryParamsValType.Field(i) + + if contains(skipFields, fieldType.Name) { + continue + } + + requestTag := getRequestTag(fieldType) + if requestTag != nil { + continue + } + + qpTag := parseQueryParamTag(fieldType) + if qpTag == nil { + continue + } + + constValue := parseConstTag(fieldType) + if constValue != nil { + values.Add(qpTag.ParamName, *constValue) + continue + } + + defaultValue := parseDefaultTag(fieldType) + + if globals != nil { + var globalFound bool + fieldType, valType, globalFound = populateFromGlobals(fieldType, valType, queryParamTagKey, globals) + if globalFound { + globalsAlreadyPopulated = append(globalsAlreadyPopulated, fieldType.Name) + } + } + + if qpTag.Serialization != "" { + vals, err := populateSerializedParams(qpTag, fieldType.Type, valType) + if err != nil { + return nil, err + } + for k, v := range vals { + values.Add(k, v) + } + } else { + switch qpTag.Style { + case "deepObject": + vals := populateDeepObjectParams(qpTag, fieldType.Type, valType) + for k, v := range vals { + for _, vv := range v { + values.Add(k, vv) + } + } + case "form": + vals := populateFormParams(qpTag, fieldType.Type, valType, ",", defaultValue, allowEmptyValue) + for k, v := range vals { + for _, vv := range v { + values.Add(k, vv) + } + } + case "pipeDelimited": + vals := populateFormParams(qpTag, fieldType.Type, valType, "|", defaultValue, allowEmptyValue) + for k, v := range vals { + for _, vv := range v { + values.Add(k, vv) + } + } + default: + return nil, fmt.Errorf("unsupported style: %s", qpTag.Style) + } + } + } + + return globalsAlreadyPopulated, nil +} + +func populateSerializedParams(tag *paramTag, objType reflect.Type, objValue reflect.Value) (map[string]string, error) { + if isNil(objType, objValue) { + return nil, nil + } + + if objType.Kind() == reflect.Pointer { + objValue = objValue.Elem() + } + + values := map[string]string{} + + switch tag.Serialization { + case "json": + data, err := json.Marshal(objValue.Interface()) + if err != nil { + return nil, fmt.Errorf("error marshaling json: %v", err) + } + values[tag.ParamName] = string(data) + } + + return values, nil +} + +func populateDeepObjectParams(tag *paramTag, objType reflect.Type, objValue reflect.Value) url.Values { + values := url.Values{} + + if isNil(objType, objValue) { + return values + } + + if objValue.Kind() == reflect.Pointer { + objValue = objValue.Elem() + } + + switch objValue.Kind() { + case reflect.Map: + // check if optionalnullable.OptionalNullable[T] + if nullableValue, ok := optionalnullable.AsOptionalNullable(objValue); ok { + // Serialize the wrapped value using the rules for its own type + if value, isSet := nullableValue.GetUntyped(); isSet && value != nil { + innerValue := reflect.ValueOf(value) + return populateDeepObjectParams(tag, innerValue.Type(), innerValue) + } + // If not set or explicitly null, skip adding to values + return values + } + + populateDeepObjectParamsMap(values, tag.ParamName, objValue) + case reflect.Struct: + populateDeepObjectParamsStruct(values, tag.ParamName, objValue) + } + + return values +} + +func populateDeepObjectParamsValue(qsValues url.Values, scope string, value reflect.Value) { + if value.Kind() == reflect.Pointer { + if value.IsNil() { + return + } + + value = value.Elem() + } + + if nullableValue, ok := optionalnullable.AsOptionalNullable(value); ok { + inner, isSet := nullableValue.GetUntyped() + if !isSet || inner == nil { + return + } + + populateDeepObjectParamsValue(qsValues, scope, reflect.ValueOf(inner)) + + return + } + + switch value.Kind() { + case reflect.Array, reflect.Slice: + populateDeepObjectParamsArray(qsValues, scope, value) + case reflect.Map: + populateDeepObjectParamsMap(qsValues, scope, value) + case reflect.Struct: + switch value.Type() { + case reflect.TypeOf(big.Int{}), reflect.TypeOf(time.Time{}), reflect.TypeOf(types.Date{}): + qsValues.Add(scope, valToString(value.Interface())) + + return + } + + populateDeepObjectParamsStruct(qsValues, scope, value) + default: + qsValues.Add(scope, valToString(value.Interface())) + } +} + +func populateDeepObjectParamsArray(qsValues url.Values, priorScope string, value reflect.Value) { + if value.Kind() != reflect.Array && value.Kind() != reflect.Slice { + return + } + + for i := 0; i < value.Len(); i++ { + qsValues.Add(priorScope, valToString(value.Index(i).Interface())) + } +} + +func populateDeepObjectParamsMap(qsValues url.Values, priorScope string, mapValue reflect.Value) { + if mapValue.Kind() != reflect.Map { + return + } + + iter := mapValue.MapRange() + + for iter.Next() { + scope := priorScope + "[" + iter.Key().String() + "]" + + populateDeepObjectParamsValue(qsValues, scope, iter.Value()) + } +} + +func populateDeepObjectParamsStruct(qsValues url.Values, priorScope string, structValue reflect.Value) { + if structValue.Kind() != reflect.Struct { + return + } + + structType := structValue.Type() + + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + fieldValue := structValue.Field(i) + + if isNil(field.Type, fieldValue) { + continue + } + + qpTag := parseQueryParamTag(field) + + if qpTag == nil { + continue + } + + scope := priorScope + + if !qpTag.Inline { + scope = priorScope + "[" + qpTag.ParamName + "]" + } + + populateDeepObjectParamsValue(qsValues, scope, fieldValue) + } +} + +func populateFormParams(tag *paramTag, objType reflect.Type, objValue reflect.Value, delimiter string, defaultValue *string, allowEmptyValue map[string]struct{}) url.Values { + return populateForm(tag.ParamName, tag.Explode, objType, objValue, delimiter, defaultValue, allowEmptyValue, func(fieldType reflect.StructField) string { + qpTag := parseQueryParamTag(fieldType) + if qpTag == nil { + return "" + } + + // When inline is true, use the parent's param name instead of the field's own name. + // This allows union/oneOf wrapper types to serialize their values directly under + // the parent's query parameter name. + if qpTag.Inline { + return tag.ParamName + } + + return qpTag.ParamName + }) +} + +type paramTag struct { + Style string + Explode bool + ParamName string + Serialization string + + // Inline is a special case for union/oneOf. When a wrapper struct type is + // used, each union/oneOf value field should be inlined (e.g. not appended + // in deepObject style with the name) as if the value was directly on the + // parent struct field. Without this annotation, the value would not be + // encoded by downstream logic that requires the struct field tag. + Inline bool +} + +func parseQueryParamTag(field reflect.StructField) *paramTag { + return parseParamTag(queryParamTagKey, field, "form", true) +} diff --git a/internal/sdk/sdkinternal/utils/requestbody.go b/internal/sdk/sdkinternal/utils/requestbody.go new file mode 100644 index 0000000..421aeb1 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/requestbody.go @@ -0,0 +1,561 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "bytes" + "context" + "fmt" + "io" + "math/big" + "mime" + "mime/multipart" + "net/textproto" + "net/url" + "path/filepath" + "reflect" + "regexp" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/optionalnullable" + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +const ( + requestTagKey = "request" + multipartFormTagKey = "multipartForm" + formTagKey = "form" +) + +var ( + jsonEncodingRegex = regexp.MustCompile(`^(application|text)\/([^+]+\+)*json.*`) + multipartEncodingRegex = regexp.MustCompile(`^multipart\/.*`) + urlEncodedEncodingRegex = regexp.MustCompile(`^application\/x-www-form-urlencoded.*`) +) + +func SerializeRequestBody(_ context.Context, request interface{}, nullable, optional bool, requestFieldName, serializationMethod, tag string) (io.Reader, string, error) { + bodyReader, contentType, err := serializeRequestBody(request, nullable, optional, requestFieldName, serializationMethod, tag) + if err != nil { + return nil, "", fmt.Errorf("error serializing request body: %w", err) + } + + if bodyReader == nil && !optional { + return nil, "", fmt.Errorf("request body is required") + } + + return bodyReader, contentType, nil +} + +func serializeRequestBody(request interface{}, nullable, optional bool, requestFieldName, serializationMethod, tag string) (io.Reader, string, error) { + requestStructType := reflect.TypeOf(request) + requestValType := reflect.ValueOf(request) + + if isNil(requestStructType, requestValType) { + if !nullable && optional { + return nil, "", nil + } + + return serializeContentType(requestFieldName, SerializationMethodToContentType[serializationMethod], requestValType, tag) + } + + if requestStructType.Kind() == reflect.Pointer { + requestStructType = requestStructType.Elem() + requestValType = requestValType.Elem() + } + + if requestStructType.Kind() != reflect.Struct { + return serializeContentType(requestFieldName, SerializationMethodToContentType[serializationMethod], requestValType, tag) + } + + requestField, ok := requestStructType.FieldByName(requestFieldName) + + if ok { + tag := getRequestTag(requestField) + if tag != nil { + // request object (non-flattened) + requestVal := requestValType.FieldByName(requestFieldName) + val := reflect.ValueOf(requestVal.Interface()) + if isNil(requestField.Type, requestVal) { + if !nullable && optional { + return nil, "", nil + } + + return serializeContentType(requestFieldName, tag.MediaType, val, string(requestField.Tag)) + } + + return serializeContentType(requestFieldName, tag.MediaType, val, string(requestField.Tag)) + } + } + + // flattened request object + return serializeContentType(requestFieldName, SerializationMethodToContentType[serializationMethod], reflect.ValueOf(request), tag) +} + +func serializeContentType(fieldName string, mediaType string, val reflect.Value, tag string) (io.Reader, string, error) { + buf := &bytes.Buffer{} + + if isNil(val.Type(), val) { + // TODO: what does a null mean for other content types? Just returning an empty buffer for now + if jsonEncodingRegex.MatchString(mediaType) { + if _, err := buf.Write([]byte("null")); err != nil { + return nil, "", err + } + } + + return buf, mediaType, nil + } + + switch { + case jsonEncodingRegex.MatchString(mediaType): + data, err := MarshalJSON(val.Interface(), reflect.StructTag(tag), true) + if err != nil { + return nil, "", err + } + + if _, err := buf.Write(data); err != nil { + return nil, "", err + } + case multipartEncodingRegex.MatchString(mediaType): + var err error + mediaType, err = encodeMultipartFormData(buf, val.Interface()) + if err != nil { + return nil, "", err + } + case urlEncodedEncodingRegex.MatchString(mediaType): + if err := encodeFormData(fieldName, buf, val.Interface()); err != nil { + return nil, "", err + } + case val.Type().Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()): + return val.Interface().(io.Reader), mediaType, nil + default: + val = reflect.Indirect(val) + + switch { + case val.Type().Kind() == reflect.String: + if _, err := buf.WriteString(valToString(val.Interface())); err != nil { + return nil, "", err + } + case reflect.TypeOf(val.Interface()) == reflect.TypeOf([]byte(nil)): + if _, err := buf.Write(val.Interface().([]byte)); err != nil { + return nil, "", err + } + default: + return nil, "", fmt.Errorf("invalid request body type %s for mediaType %s", val.Type(), mediaType) + } + } + + return buf, mediaType, nil +} + +func encodeMultipartFormData(w io.Writer, data interface{}) (string, error) { + requestStructType := reflect.TypeOf(data) + requestValType := reflect.ValueOf(data) + + if requestStructType.Kind() == reflect.Pointer { + requestStructType = requestStructType.Elem() + requestValType = requestValType.Elem() + } + + writer := multipart.NewWriter(w) + + for i := 0; i < requestStructType.NumField(); i++ { + field := requestStructType.Field(i) + fieldType := field.Type + valType := requestValType.Field(i) + + if isNil(fieldType, valType) { + continue + } + + if fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + valType = valType.Elem() + } + + tag := parseMultipartFormTag(field) + + // Explicit null is representable only in JSON-tagged parts, whose + // value is a JSON document; ordinary parts have no null encoding, + // so unset, typed-nil and non-JSON explicit-null wrappers are omitted + if nullableValue, ok := optionalnullable.AsOptionalNullable(valType); ok { + inner, isSet := nullableValue.GetUntyped() + if !isSet { + continue + } + + if inner == nil { + if !tag.JSON { + continue + } + } else { + valType = reflect.ValueOf(inner) + fieldType = valType.Type() + + if isNil(fieldType, valType) { + continue + } + + if fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + valType = valType.Elem() + } + } + } + + if tag.File { + switch fieldType.Kind() { + case reflect.Slice, reflect.Array: + for i := 0; i < valType.Len(); i++ { + arrayVal := valType.Index(i) + + if err := encodeMultipartFormDataFile(writer, tag.Name, arrayVal.Type(), arrayVal); err != nil { + writer.Close() + return "", err + } + } + default: + if err := encodeMultipartFormDataFile(writer, tag.Name, fieldType, valType); err != nil { + writer.Close() + return "", err + } + } + } else if tag.JSON { + jw, err := writer.CreateFormField(tag.Name) + if err != nil { + writer.Close() + return "", err + } + d, err := MarshalJSON(valType.Interface(), field.Tag, true) + if err != nil { + writer.Close() + return "", err + } + if _, err := jw.Write(d); err != nil { + writer.Close() + return "", err + } + } else { + switch fieldType.Kind() { + case reflect.Slice, reflect.Array: + values := parseDelimitedArray(true, valType, ",") + for _, v := range values { + if err := writer.WriteField(tag.Name, v); err != nil { + writer.Close() + return "", err + } + } + default: + if err := writer.WriteField(tag.Name, valToString(valType.Interface())); err != nil { + writer.Close() + return "", err + } + } + } + } + + if err := writer.Close(); err != nil { + return "", err + } + + return writer.FormDataContentType(), nil +} + +func encodeMultipartFormDataFile(w *multipart.Writer, fieldName string, fieldType reflect.Type, valType reflect.Value) error { + if fieldType.Kind() != reflect.Struct { + return fmt.Errorf("invalid type %s for multipart/form-data file", valType.Type()) + } + + var fileName string + var reader io.Reader + + for i := 0; i < fieldType.NumField(); i++ { + field := fieldType.Field(i) + val := valType.Field(i) + + tag := parseMultipartFormTag(field) + if !tag.Content && tag.Name == "" { + continue + } + + if tag.Content && val.CanInterface() { + if reflect.TypeOf(val.Interface()) == reflect.TypeOf([]byte(nil)) { + reader = bytes.NewReader(val.Interface().([]byte)) + } else if reflect.TypeOf(val.Interface()).Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) { + reader = val.Interface().(io.Reader) + } + } else { + fileName = val.String() + } + } + + if fileName == "" || reader == nil { + return fmt.Errorf("invalid multipart/form-data file") + } + + // Detect content type based on file extension + contentType := mime.TypeByExtension(filepath.Ext(fileName)) + if contentType == "" { + contentType = "application/octet-stream" + } + + // Create multipart header with proper content type + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileName)) + h.Set("Content-Type", contentType) + + fw, err := w.CreatePart(h) + if err != nil { + return err + } + if _, err := io.Copy(fw, reader); err != nil { + return err + } + + // Reset seek position to 0 if the reader supports seeking + if seeker, ok := reader.(io.Seeker); ok { + if _, err := seeker.Seek(0, io.SeekStart); err != nil { + return err + } + } + + return nil +} + +func encodeFormData(fieldName string, w io.Writer, data interface{}) error { + requestType := reflect.TypeOf(data) + requestValType := reflect.ValueOf(data) + + if requestType.Kind() == reflect.Pointer { + requestType = requestType.Elem() + requestValType = requestValType.Elem() + } + + dataValues := url.Values{} + + switch requestType.Kind() { + case reflect.Struct: + for i := 0; i < requestType.NumField(); i++ { + field := requestType.Field(i) + fieldType := field.Type + valType := requestValType.Field(i) + + if isNil(fieldType, valType) { + continue + } + + if fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + valType = valType.Elem() + } + + tag := parseFormTag(field) + + // Explicit null is representable only in JSON-tagged fields, whose + // value is a JSON document; ordinary form fields have no null + // encoding, so unset, typed-nil and non-JSON explicit-null + // wrappers are omitted + if nullableValue, ok := optionalnullable.AsOptionalNullable(valType); ok { + inner, isSet := nullableValue.GetUntyped() + if !isSet { + continue + } + + if inner == nil { + if !tag.JSON { + continue + } + } else { + valType = reflect.ValueOf(inner) + fieldType = valType.Type() + + if isNil(fieldType, valType) { + continue + } + + if fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + valType = valType.Elem() + } + } + } + + if tag.JSON { + data, err := MarshalJSON(valType.Interface(), field.Tag, true) + if err != nil { + return err + } + dataValues.Set(tag.Name, string(data)) + } else { + switch tag.Style { + // TODO: support other styles + case "form": + values := populateForm(tag.Name, tag.Explode, fieldType, valType, ",", nil, nil, func(sf reflect.StructField) string { + tag := parseFormTag(field) + if tag == nil { + return "" + } + + return tag.Name + }) + for k, v := range values { + for _, vv := range v { + dataValues.Add(k, vv) + } + } + } + } + } + case reflect.Map: + // check if optionalnullable.OptionalNullable[T] + if nullableValue, ok := optionalnullable.AsOptionalNullable(requestValType); ok { + // Serialize the wrapped value using the rules for its own type + if value, isSet := nullableValue.GetUntyped(); isSet && value != nil { + innerValue := reflect.ValueOf(value) + + switch innerValue.Kind() { + case reflect.Map, reflect.Struct: + switch innerValue.Interface().(type) { + case time.Time, types.Date, big.Int: + default: + return encodeFormData(fieldName, w, value) + } + } + + values := populateForm(fieldName, false, innerValue.Type(), innerValue, ",", nil, nil, func(sf reflect.StructField) string { + tag := parseFormTag(sf) + if tag == nil { + return "" + } + + return tag.Name + }) + for k, v := range values { + for _, vv := range v { + dataValues.Add(k, vv) + } + } + + break + } + // If not set or explicitly null, skip adding to form + break + } + + // Handle regular map + for _, k := range requestValType.MapKeys() { + v := requestValType.MapIndex(k) + dataValues.Set(fmt.Sprintf("%v", k.Interface()), valToString(v.Interface())) + } + case reflect.Slice, reflect.Array: + for i := 0; i < requestValType.Len(); i++ { + v := requestValType.Index(i) + dataValues.Set(fieldName, valToString(v.Interface())) + } + } + + if _, err := w.Write([]byte(dataValues.Encode())); err != nil { + return err + } + + return nil +} + +type requestTag struct { + MediaType string +} + +func getRequestTag(field reflect.StructField) *requestTag { + // example `request:"mediaType=multipart/form-data"` + values := parseStructTag(requestTagKey, field) + if values == nil { + return nil + } + + tag := &requestTag{ + MediaType: "application/octet-stream", + } + + for k, v := range values { + switch k { + case "mediaType": + tag.MediaType = v + } + } + + return tag +} + +type multipartFormTag struct { + File bool + Content bool + JSON bool + Name string +} + +func parseMultipartFormTag(field reflect.StructField) *multipartFormTag { + // example `multipartForm:"name=file"` + values := parseStructTag(multipartFormTagKey, field) + + tag := &multipartFormTag{} + + for k, v := range values { + switch k { + case "file": + tag.File = v == "true" + case "content": + tag.Content = v == "true" + case "name": + tag.Name = v + case "json": + tag.JSON = v == "true" + } + } + + return tag +} + +type formTag struct { + Name string + JSON bool + Style string + Explode bool +} + +func parseFormTag(field reflect.StructField) *formTag { + // example `form:"name=propName,style=spaceDelimited,explode"` + values := parseStructTag(formTagKey, field) + + tag := &formTag{ + Style: "form", + Explode: true, + } + + for k, v := range values { + switch k { + case "name": + tag.Name = v + case "json": + tag.JSON = v == "true" + case "style": + tag.Style = v + case "explode": + tag.Explode = v == "true" + } + } + + return tag +} diff --git a/internal/sdk/sdkinternal/utils/retries.go b/internal/sdk/sdkinternal/utils/retries.go new file mode 100644 index 0000000..81eaa08 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/retries.go @@ -0,0 +1,399 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "context" + "errors" + "fmt" + "io" + "math" + "math/rand" + "net" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + "syscall" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" +) + +// Deprecated: Use retry.BackoffStrategy instead. +type BackoffStrategy = retry.BackoffStrategy + +// Deprecated: Use retry.Config instead. +type RetryConfig = retry.Config + +type Retries struct { + Config *retry.Config + StatusCodes []string +} + +var ( + // IETF RFC 7231 4.2 safe and idempotent HTTP methods for connection retries + idempotentHTTPMethods = []string{ + http.MethodDelete, + http.MethodGet, + http.MethodHead, + http.MethodOptions, + http.MethodPut, + } +) + +func Retry(ctx context.Context, r Retries, operation func(attempt int) (*http.Response, error)) (*http.Response, error) { + switch r.Config.Strategy { + case "backoff", "attempt-count-backoff": + if r.Config.Backoff == nil { + return operation(0) + } + + var resp *http.Response + if r.Config.Strategy == "attempt-count-backoff" { + err := retryWithAttemptCountBackoff(ctx, r.Config, func(attempt int) error { + return retryOperation(ctx, r, operation, &resp, attempt) + }) + + return retryResult(resp, err) + } + + err := retryWithBackoff(ctx, r.Config.Backoff, func() error { + return retryOperation(ctx, r, operation, &resp, 0) + }) + + return retryResult(resp, err) + default: + return operation(0) + } +} + +func retryResult(resp *http.Response, err error) (*http.Response, error) { + var tempErr *retry.TemporaryError + if err != nil && !errors.As(err, &tempErr) { + if resp != nil { + resp.Body.Close() + } + return nil, err + } + + if resp == nil { + return nil, err + } + + return resp, nil +} + +func retryOperation(ctx context.Context, r Retries, operation func(attempt int) (*http.Response, error), resp **http.Response, attempt int) error { + if *resp != nil { + (*resp).Body.Close() + *resp = nil + } + + select { + case <-ctx.Done(): + return retry.Permanent(ctx.Err()) + default: + } + + res, err := operation(attempt) + if err != nil { + if !r.Config.RetryConnectionErrors { + return retry.Permanent(err) + } + + var httpMethod string + + // Use http.Request method if available + if res != nil && res.Request != nil { + httpMethod = res.Request.Method + } + + isIdempotentHTTPMethod := slices.Contains(idempotentHTTPMethods, httpMethod) + urlError := new(url.Error) + + if errors.As(err, &urlError) { + if urlError.Temporary() || urlError.Timeout() { + return err + } + + // In certain error cases, the http.Request may not have + // been populated, so use url.Error.Op which only has its + // first character capitalized from the original request + // HTTP method. + if httpMethod == "" { + httpMethod = strings.ToUpper(urlError.Op) + } + + isIdempotentHTTPMethod = slices.Contains(idempotentHTTPMethods, httpMethod) + + // Connection closed + if errors.Is(urlError.Err, io.EOF) && isIdempotentHTTPMethod { + return err + } + } + + var networkOperationError *net.OpError + isBrokenPipeOrConnectionReset := errors.As(err, &networkOperationError) && + (errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET)) + + if isBrokenPipeOrConnectionReset && isIdempotentHTTPMethod { + return err + } + + return retry.Permanent(err) + } + *resp = res + if res == nil { + return fmt.Errorf("no response") + } + + if shouldRetryResponse(res, r.StatusCodes) { + return retry.TemporaryFromResponse("request failed", res) + } + + *resp = res + + return nil +} + +func shouldRetryResponse(res *http.Response, statusCodes []string) bool { + for _, code := range statusCodes { + if strings.Contains(strings.ToUpper(code), "X") { + codeRange, err := strconv.Atoi(code[:1]) + if err != nil { + continue + } + + s := res.StatusCode / 100 + + if s >= codeRange && s < codeRange+1 { + return true + } + } else { + parsedCode, err := strconv.Atoi(code) + if err != nil { + continue + } + + if res.StatusCode == parsedCode { + return true + } + } + } + + return false +} + +func retryWithBackoff(ctx context.Context, s *retry.BackoffStrategy, operation func() error) error { + var ( + err error + attempt int + start = time.Now() + maxElapsedTime = time.Duration(s.MaxElapsedTime) * time.Millisecond + ) + + timer := &defaultTimer{} + defer func() { + timer.Stop() + }() + + for { + var next time.Duration + err = operation() + if err == nil { + return nil + } + + var permanent *retry.PermanentError + if errors.As(err, &permanent) { + return permanent.Unwrap() + } + + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + + if time.Since(start) >= maxElapsedTime { + return err + } + + var temporary *retry.TemporaryError + hasRetryAfter := false + if errors.As(err, &temporary) { + next = temporary.RetryAfter() + hasRetryAfter = next > 0 + } + + if hasRetryAfter && next > maxElapsedTime-time.Since(start) { + return err + } + + if next <= 0 { + next = nextInterval(s, attempt) + } + + timer.Start(next) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C(): + } + + attempt += 1 + } +} + +func retryWithAttemptCountBackoff(ctx context.Context, c *retry.Config, operation func(attempt int) error) error { + var ( + err error + attempt int + ) + + timer := &defaultTimer{} + defer func() { + timer.Stop() + }() + + maxRetries := 0 + if c.MaxRetries != nil { + maxRetries = *c.MaxRetries + } + + for { + var next time.Duration + err = operation(attempt) + if err == nil { + return nil + } + + var permanent *retry.PermanentError + if errors.As(err, &permanent) { + return permanent.Unwrap() + } + + if attempt >= maxRetries { + return err + } + + var temporary *retry.TemporaryError + if errors.As(err, &temporary) { + next = temporary.RetryAfter() + } + + if next <= 0 { + next = nextAttemptCountInterval(c.Backoff, attempt) + } + + timer.Start(next) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C(): + } + + attempt += 1 + } +} + +type Timer interface { + Start(duration time.Duration) + Stop() + C() <-chan time.Time +} + +// defaultTimer implements Timer interface using time.Timer +type defaultTimer struct { + timer *time.Timer +} + +// C returns the timers channel which receives the current time when the timer fires. +func (t *defaultTimer) C() <-chan time.Time { + return t.timer.C +} + +// Start starts the timer to fire after the given duration +func (t *defaultTimer) Start(duration time.Duration) { + if t.timer == nil { + t.timer = time.NewTimer(duration) + return + } + + if !t.timer.Stop() { + select { + case <-t.timer.C: + default: + } + } + + t.timer.Reset(duration) +} + +// Stop is called when the timer is not used anymore and resources may be freed. +func (t *defaultTimer) Stop() { + if t.timer != nil { + t.timer.Stop() + } +} + +func nextInterval(s *retry.BackoffStrategy, attempt int) time.Duration { + initialInterval := float64(time.Duration(s.InitialInterval) * time.Millisecond) + maxInterval := float64(time.Duration(s.MaxInterval) * time.Millisecond) + exponent := s.Exponent + jitterFactor := float64(0.25) + + interval := initialInterval * math.Pow(float64(attempt+1), exponent) + + jitter := rand.Float64() * jitterFactor * interval + if rand.Float64() < 0.5 { + jitter = -1 * jitter + } + + interval = interval + jitter + + if interval <= 0 { + interval = initialInterval + } + + if interval > maxInterval { + interval = maxInterval + } + + return time.Duration(interval) +} + +func nextAttemptCountInterval(s *retry.BackoffStrategy, attempt int) time.Duration { + initialInterval := float64(time.Duration(s.InitialInterval) * time.Millisecond) + maxInterval := float64(time.Duration(s.MaxInterval) * time.Millisecond) + exponent := s.Exponent + + interval := initialInterval * math.Pow(exponent, float64(attempt)) + interval *= 1 - (rand.Float64() * 0.25) + + if interval <= 0 { + interval = initialInterval + } + + if interval > maxInterval { + interval = maxInterval + } + + return time.Duration(interval) +} diff --git a/internal/sdk/sdkinternal/utils/security.go b/internal/sdk/sdkinternal/utils/security.go new file mode 100644 index 0000000..d25fc6a --- /dev/null +++ b/internal/sdk/sdkinternal/utils/security.go @@ -0,0 +1,339 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "reflect" + "strings" +) + +const ( + securityTagKey = "security" +) + +type securityTag struct { + Option bool + Scheme bool + Composite bool + Name string + Type string + SubType string + Env string +} + +func PopulateSecurity(ctx context.Context, req *http.Request, securitySource func(context.Context) (interface{}, error), allowedFields ...string) error { + if securitySource == nil { + return nil + } + + security, err := securitySource(ctx) + if err != nil { + return err + } + + headers := make(map[string]string) + queryParams := make(map[string]string) + + securityValType := trueReflectValue(reflect.ValueOf(security)) + securityStructType := securityValType.Type() + + if isNil(securityStructType, securityValType) { + return nil + } + + if securityStructType.Kind() == reflect.Ptr { + securityStructType = securityStructType.Elem() + securityValType = securityValType.Elem() + } + + populateSecurityFields(headers, queryParams, securityStructType, securityValType, security, allowedFields) + + for key, value := range headers { + req.Header.Add(key, value) + } + + query := req.URL.Query() + for key, value := range queryParams { + query.Add(key, value) + } + req.URL.RawQuery = query.Encode() + + return nil +} + +func populateSecurityFields(headers, queryParams map[string]string, securityStructType reflect.Type, securityValType reflect.Value, security interface{}, allowedFields []string) { + type fieldPair struct { + fieldType reflect.StructField + valType reflect.Value + } + + var fields []fieldPair + if len(allowedFields) > 0 { + for _, name := range allowedFields { + ft, ok := securityStructType.FieldByName(name) + if !ok { + continue + } + fields = append(fields, fieldPair{ft, securityValType.FieldByName(name)}) + } + } else { + for i := 0; i < securityStructType.NumField(); i++ { + fields = append(fields, fieldPair{securityStructType.Field(i), securityValType.Field(i)}) + } + } + + for _, f := range fields { + kind := f.valType.Kind() + + if isNil(f.fieldType.Type, f.valType) { + continue + } + + if f.fieldType.Type.Kind() == reflect.Pointer { + kind = f.valType.Elem().Kind() + } + + secTag := parseSecurityTag(f.fieldType) + if secTag == nil { + continue + } + + if secTag.Option { + handleSecurityOption(headers, queryParams, f.valType.Interface()) + return + } else if secTag.Scheme { + // Special case for basic auth which could be a flattened struct + if secTag.SubType == "basic" && kind != reflect.Struct { + parseSecurityScheme(headers, queryParams, secTag, security) + } else { + parseSecurityScheme(headers, queryParams, secTag, f.valType.Interface()) + } + + if !secTag.Composite { + return + } + } + } +} + +func handleSecurityOption(headers, queryParams map[string]string, option interface{}) { + optionValType := trueReflectValue(reflect.ValueOf(option)) + optionStructType := optionValType.Type() + + if isNil(optionStructType, optionValType) { + return + } + + for i := 0; i < optionStructType.NumField(); i++ { + fieldType := optionStructType.Field(i) + valType := optionValType.Field(i) + + secTag := parseSecurityTag(fieldType) + if secTag == nil || !secTag.Scheme { + continue + } + + if secTag.Type == "http" && secTag.SubType == "basic" && valType.Kind() != reflect.Struct { + handleBasicAuthScheme(headers, optionValType.Interface()) + return + } + + parseSecurityScheme(headers, queryParams, secTag, valType.Interface()) + } +} + +func parseSecurityScheme(headers, queryParams map[string]string, schemeTag *securityTag, scheme interface{}) { + schemeVal := trueReflectValue(reflect.ValueOf(scheme)) + schemeType := schemeVal.Type() + + if isNil(schemeType, schemeVal) { + return + } + + if schemeType.Kind() == reflect.Struct { + if schemeTag.Type == "http" { + switch schemeTag.SubType { + case "basic": + handleBasicAuthScheme(headers, schemeVal.Interface()) + return + case "custom": + return + } + } + + for i := 0; i < schemeType.NumField(); i++ { + fieldType := schemeType.Field(i) + valType := schemeVal.Field(i) + + if isNil(fieldType.Type, valType) { + continue + } + + if fieldType.Type.Kind() == reflect.Ptr { + valType = valType.Elem() + } + + secTag := parseSecurityTag(fieldType) + if secTag == nil || secTag.Name == "" { + return + } + + parseSecuritySchemeValue(headers, queryParams, schemeTag, secTag, valType.Interface()) + } + } else { + parseSecuritySchemeValue(headers, queryParams, schemeTag, schemeTag, schemeVal.Interface()) + } +} + +func parseSecuritySchemeValue(headers, queryParams map[string]string, schemeTag *securityTag, secTag *securityTag, val interface{}) { + switch schemeTag.Type { + case "apiKey": + switch schemeTag.SubType { + case "header": + headers[secTag.Name] = valToString(val) + case "query": + queryParams[secTag.Name] = valToString(val) + case "cookie": + headers["Cookie"] = fmt.Sprintf("%s=%s", secTag.Name, valToString(val)) + default: + panic("not supported") + } + case "openIdConnect": + headers[secTag.Name] = prefixBearer(valToString(val)) + case "oauth2": + if schemeTag.SubType != "client_credentials" { + headers[secTag.Name] = prefixBearer(valToString(val)) + } + case "http": + switch schemeTag.SubType { + case "bearer": + headers[secTag.Name] = prefixBearer(valToString(val)) + case "basic": + headers[secTag.Name] = valToString(val) + case "custom": + default: + panic("not supported") + } + default: + panic("not supported") + } +} + +func prefixBearer(authHeaderValue string) string { + if strings.HasPrefix(strings.ToLower(authHeaderValue), "bearer ") { + return authHeaderValue + } + + return fmt.Sprintf("Bearer %s", authHeaderValue) +} + +func handleBasicAuthScheme(headers map[string]string, scheme interface{}) { + schemeStructType := reflect.TypeOf(scheme) + schemeValType := reflect.ValueOf(scheme) + + var username, password string + + for i := 0; i < schemeStructType.NumField(); i++ { + fieldType := schemeStructType.Field(i) + valType := schemeValType.Field(i) + + if fieldType.Type.Kind() == reflect.Ptr { + valType = valType.Elem() + } + + secTag := parseSecurityTag(fieldType) + if secTag == nil || secTag.Name == "" { + continue + } + + switch secTag.Name { + case "username": + username = valType.String() + case "password": + password = valType.String() + } + } + + headers["Authorization"] = fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))) +} + +func parseSecurityTag(field reflect.StructField) *securityTag { + tag := field.Tag.Get(securityTagKey) + if tag == "" { + return nil + } + + option := false + scheme := false + composite := false + name := "" + securityType := "" + securitySubType := "" + env := "" + + options := strings.Split(tag, ",") + for _, optionConf := range options { + parts := strings.Split(optionConf, "=") + if len(parts) < 1 || len(parts) > 2 { + continue + } + + switch parts[0] { + case "name": + name = parts[1] + case "type": + securityType = parts[1] + case "subtype": + securitySubType = parts[1] + case "option": + option = true + case "scheme": + scheme = true + case "composite": + composite = true + case "env": + env = parts[1] + } + } + + return &securityTag{ + Option: option, + Scheme: scheme, + Composite: composite, + Name: name, + Type: securityType, + SubType: securitySubType, + Env: env, + } +} + +func trueReflectValue(val reflect.Value) reflect.Value { + kind := val.Type().Kind() + for kind == reflect.Interface || kind == reflect.Ptr { + innerVal := val.Elem() + if !innerVal.IsValid() { + break + } + val = innerVal + kind = val.Type().Kind() + } + return val +} diff --git a/internal/sdk/sdkinternal/utils/union.go b/internal/sdk/sdkinternal/utils/union.go new file mode 100644 index 0000000..a12e131 --- /dev/null +++ b/internal/sdk/sdkinternal/utils/union.go @@ -0,0 +1,302 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "encoding/json" + "math/big" + "reflect" + "strings" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/types" +) + +// UnionCandidate represents a candidate type during union deserialization +type UnionCandidate struct { + Matched int // Count of matched fields (includes inexact) + Inexact int // Count of fields with unknown/unrecognized enum values + Unmatched int // Count of struct fields not found in raw JSON + AdditionalProperties int // Count of fields captured by additionalProperties + Type any // The union type enum value + Value any // The unmarshaled value +} + +// PickBestUnionCandidate selects the best union type candidate according to `betterCandidate` +func PickBestUnionCandidate(candidates []UnionCandidate, rawJSON []byte) *UnionCandidate { + if len(candidates) == 0 { + return nil + } + + if len(candidates) == 1 { + return &candidates[0] + } + + var raw any + _ = json.Unmarshal(rawJSON, &raw) + + var best *UnionCandidate + for i := range candidates { + countFields(&candidates[i], raw) + best = betterCandidate(best, &candidates[i]) + } + return best +} + +// betterCandidate returns the better of two candidates based on: +// 1. Matched count (higher is better) +// 2. Inexact count (lower is better) +// 3. Unmatched count (lower is better - fewer zero defaulted values) +// 4. AdditionalProperties count (higher is better - captures more extra fields) +// Returns a if tied (preserving spec order). +func betterCandidate(a, b *UnionCandidate) *UnionCandidate { + if a == nil { + return b + } + if b == nil { + return a + } + if a.Matched != b.Matched { + if a.Matched > b.Matched { + return a + } + return b + } + if a.Inexact != b.Inexact { + if a.Inexact > b.Inexact { + return b + } + return a + } + if a.Unmatched != b.Unmatched { + if a.Unmatched > b.Unmatched { + return b + } + return a + } + if a.AdditionalProperties != b.AdditionalProperties { + if a.AdditionalProperties > b.AdditionalProperties { + return a + } + return b + } + return a +} + +// countFields populates UnionCandidate.Matched, UnionCandidate.Inexact, and UnionCandidate.Unmatched fields +func countFields(candidate *UnionCandidate, raw any) { + typ := reflect.TypeOf(candidate.Value) + val := reflect.ValueOf(candidate.Value) + countFieldsRecursive(candidate, typ, val, raw) +} + +func countFieldsRecursive(candidate *UnionCandidate, typ reflect.Type, val reflect.Value, raw any) { + kind := typ.Kind() + + // Handle interface{}/any types - can hold any JSON value + if kind == reflect.Interface { + candidate.Matched++ + return + } + + if typ.Kind() == reflect.Ptr { + if raw == nil { + // Handle null JSON value match + candidate.Matched++ + return + } + typ, val = dereferencePointers(typ, val) + kind = typ.Kind() + } + + // Handle primitives + if kind == reflect.String || + kind == reflect.Bool || + kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 || + kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 || kind == reflect.Uint64 || + kind == reflect.Float32 || kind == reflect.Float64 || + typ == reflect.TypeOf(time.Time{}) || + typ == reflect.TypeOf(big.Int{}) || + typ == reflect.TypeOf(types.Date{}) || + typ == reflect.TypeOf([]byte{}) { + candidate.Matched++ + if !isExact(val) || raw == nil { + candidate.Inexact++ + } + return + } + + // Handle unions + if isUnion, activeVariant, variantVal := findActiveUnionVariant(typ, val); isUnion { + if activeVariant != nil { + countFieldsRecursive(candidate, activeVariant.Type, variantVal, raw) + } + return + } + + // Handle regular structs + if kind == reflect.Struct { + rawObj, ok := raw.(map[string]any) + if !ok { + return + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldVal := val.Field(i) + + if field.Tag.Get("additionalProperties") == "true" { + if field.Type.Kind() == reflect.Map && !fieldVal.IsNil() { + candidate.AdditionalProperties += fieldVal.Len() + } + continue + } + + jsonName, ok := jsonFieldName(field) + if !ok { + continue + } + + rawField, exists := rawObj[jsonName] + if !exists { + candidate.Unmatched++ + continue + } + + countFieldsRecursive(candidate, field.Type, fieldVal, rawField) + } + return + } + + // Handle slices and arrays + if kind == reflect.Slice || kind == reflect.Array { + if val.IsNil() || val.Len() == 0 { + return + } + + rawArr, ok := raw.([]any) + if !ok { + return + } + + // Count each array/slice element + for i := 0; i < val.Len() && i < len(rawArr); i++ { + itemVal := val.Index(i) + countFieldsRecursive(candidate, itemVal.Type(), itemVal, rawArr[i]) + } + return + } + + // Handle maps + if kind == reflect.Map { + if val.IsNil() || val.Len() == 0 { + return + } + + rawObj, ok := raw.(map[string]any) + if !ok { + return + } + + for _, key := range val.MapKeys() { + keyStr := key.String() + rawVal, exists := rawObj[keyStr] + if exists { + mapVal := val.MapIndex(key) + countFieldsRecursive(candidate, mapVal.Type(), mapVal, rawVal) + } + } + return + } + // Anything else + candidate.Matched++ + return +} + +// jsonFieldName returns the JSON field name for a struct field. +// Returns ("", false) if the field should be skipped (json:"-"). +func jsonFieldName(field reflect.StructField) (string, bool) { + jsonTag := field.Tag.Get("json") + if jsonTag == "-" { + return "", false + } + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + if parts[0] != "" { + return parts[0], true + } + } + return field.Name, true +} + +// findActiveUnionVariant detects if a struct is a union type and returns the active variant. +// A union type is detected by having fields with the `union:"member"` tag. +// Returns (false, nil, invalid) if not a union type. +// Returns (true, nil, invalid) if union type but no active variant found (all nil). +// Returns (true, field, value) if union type with an active variant. +func findActiveUnionVariant(typ reflect.Type, val reflect.Value) (bool, *reflect.StructField, reflect.Value) { + if typ.Kind() != reflect.Struct { + return false, nil, reflect.Value{} + } + + var activeVariant *reflect.StructField + var activeValue reflect.Value + isUnion := false + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + // Look for fields tagged as union members + if field.Tag.Get("union") != "member" { + continue + } + + isUnion = true + + fieldVal := val.Field(i) + if !fieldVal.IsNil() { + activeVariant = &field + activeValue = fieldVal + } + } + + return isUnion, activeVariant, activeValue +} + +func isExact(val reflect.Value) bool { + if !val.IsValid() { + return true + } + + // If not addressable, make an addressable copy + if !val.CanAddr() && val.CanInterface() { + ptr := reflect.New(val.Type()) + ptr.Elem().Set(val) + val = ptr.Elem() + } + + if val.CanInterface() && val.CanAddr() { + ptrVal := val.Addr() + if method := ptrVal.MethodByName("IsExact"); method.IsValid() { + results := method.Call(nil) + if len(results) == 1 && results[0].Kind() == reflect.Bool { + return results[0].Bool() + } + } + } + return true +} diff --git a/internal/sdk/sdkinternal/utils/union_test.go b/internal/sdk/sdkinternal/utils/union_test.go new file mode 100644 index 0000000..c1a8efc --- /dev/null +++ b/internal/sdk/sdkinternal/utils/union_test.go @@ -0,0 +1,695 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "encoding/json" + "reflect" + "testing" +) + +func requireNotNil(t *testing.T, result *UnionCandidate) { + t.Helper() + if result == nil { + t.Fatal("expected non-nil result") + } +} + +func assertIsType(t *testing.T, want, got any) { + t.Helper() + if reflect.TypeOf(got) != reflect.TypeOf(want) { + t.Errorf("expected type %T, got %T", want, got) + } +} + +func assertTrue(t *testing.T, cond bool, msg string) { + t.Helper() + if !cond { + t.Error(msg) + } +} + +// makeCandidates unmarshals payload into each type and returns candidates +func makeCandidates(t *testing.T, payload string, types ...any) []UnionCandidate { + t.Helper() + candidates := make([]UnionCandidate, len(types)) + for i, typ := range types { + val := reflect.New(reflect.TypeOf(typ)).Interface() + if err := UnmarshalJSON([]byte(payload), val, "", false, nil); err != nil { + t.Fatal(err) + } + candidates[i] = UnionCandidate{Type: typ, Value: reflect.ValueOf(val).Elem().Interface()} + } + return candidates +} + +func TestPickBestUnionCandidate_SelectsTypeWithMoreMatchedFields(t *testing.T) { + type A struct { + Foo string `json:"foo"` + } + type B struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + + payload := `{"foo": "", "bar": ""}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_EmptyCandidates(t *testing.T) { + result := PickBestUnionCandidate([]UnionCandidate{}, []byte(`{"foo": "test"}`)) + if result != nil { + t.Errorf("expected nil result, got %v", result) + } +} + +func TestPickBestUnionCandidate_PrefersFewerUnmatchedFields(t *testing.T) { + type A struct { + Foo string `json:"foo"` + Bar string `json:"bar"` // not in payload + } + type B struct { + Foo string `json:"foo"` + } + + payload := `{"foo": "test"}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) // fewer unmatched fields +} + +func TestPickBestUnionCandidate_NestedStructs(t *testing.T) { + type InnerA struct { + Value string `json:"value"` + } + type InnerB struct { + Value string `json:"value"` + Extra string `json:"extra"` + } + type A struct { + Nested InnerA `json:"nested"` + } + type B struct { + Nested InnerB `json:"nested"` + } + + payload := `{"nested": {"value": "test", "extra": "data"}}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_ArrayFields(t *testing.T) { + type ItemA struct { + Name string `json:"name"` + } + type ItemB struct { + Name string `json:"name"` + Value string `json:"value"` + } + type A = []ItemA + type B = []ItemB + + payload := `[{"name": "a", "value": "1"}, {"name": "b", "value": "2"}]` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_PreservesOrderOnTie(t *testing.T) { + type A struct { + Foo string `json:"foo"` + } + type B struct { + Foo string `json:"foo"` + } + + payload := `{"foo": "test"}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, A{}, result.Type) // first wins on tie +} + +func TestPickBestUnionCandidate_OptionalPointerFields(t *testing.T) { + type A struct { + Foo *string `json:"foo"` + } + type B struct { + Foo *string `json:"foo"` + Bar *string `json:"bar"` + } + + payload := `{"foo": "test", "bar": "value"}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_OptionalPointerStructs(t *testing.T) { + type InnerA struct { + Name string `json:"name"` + } + type InnerB struct { + Name string `json:"name"` + Value string `json:"value"` + } + type A struct { + Nested *InnerA `json:"nested"` + } + type B struct { + Nested *InnerB `json:"nested"` + } + + payload := `{"nested": {"name": "test", "value": "data"}}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_NullPointerField(t *testing.T) { + type A struct { + Bar *string `json:"bar"` + } + type B struct { + Foo *string `json:"foo"` + } + + payload := `{"foo": null}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_NullNestedPointerField(t *testing.T) { + type InnerA struct { + Bar *string `json:"bar"` + } + type InnerB struct { + Bar *bool `json:"bar"` + } + type A struct { + Foo InnerA `json:"foo"` + } + type B struct { + Foo InnerB `json:"foo"` + } + + payload := `{"foo": {"bar": null}}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, A{}, result.Type) // first wins on tie +} + +func TestPickBestUnionCandidate_NullNonPointerField(t *testing.T) { + type A struct { + Foo string `json:"foo"` + } + type B struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + + payload := `{"foo": "", "bar": null}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_NullNestedFieldDifferentStructs(t *testing.T) { + type InnerA struct { + Bar string `json:"bar"` + } + type InnerB struct { + Baz *string `json:"baz"` + } + type A struct { + Foo InnerA `json:"foo"` + } + type B struct { + Foo InnerB `json:"foo"` + } + + payload := `{"foo": {"baz": null}}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_NullNestedFieldBothPresent(t *testing.T) { + type InnerA struct { + Bar *string `json:"bar"` + } + type InnerB struct { + Baz *string `json:"baz"` + } + type A struct { + Foo InnerA `json:"foo"` + } + type B struct { + Foo InnerB `json:"foo"` + } + + payload := `{"foo": {"bar": null, "baz": null}}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, A{}, result.Type) // first wins on tie +} + +// EnumA represents an enum with values 1 or 2 +type EnumA int + +func (e *EnumA) IsExact() bool { + return *e == 1 || *e == 2 +} + +func (e *EnumA) UnmarshalJSON(data []byte) error { + var v int + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *e = EnumA(v) + return nil +} + +// EnumB represents an enum with values 3 or 4 +type EnumB int + +func (e *EnumB) IsExact() bool { + return *e == 3 || *e == 4 +} + +func (e *EnumB) UnmarshalJSON(data []byte) error { + var v int + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *e = EnumB(v) + return nil +} + +func TestPickBestUnionCandidate_EnumDiscrimination(t *testing.T) { + type A struct { + Val EnumA `json:"a"` + } + type B struct { + Val EnumB `json:"a"` + } + + payload := `{"a": 4}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_ThreeWayFieldDiscrimination(t *testing.T) { + type A struct { + FieldA string `json:"a"` + FieldB string `json:"b"` + } + type B struct { + FieldA string `json:"a"` + FieldC string `json:"c"` + } + type C struct { + FieldB string `json:"b"` + FieldC string `json:"c"` + } + + payload := `{"b": "", "c": ""}` + candidates := makeCandidates(t, payload, A{}, B{}, C{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, C{}, result.Type) +} + +func TestPickBestUnionCandidate_ConstFieldDiscrimination(t *testing.T) { + type A struct { + ConstA string `json:"a" const:"x"` + ConstB string `json:"b" const:"1"` + } + type B struct { + ConstA string `json:"a" const:"x"` + ConstC string `json:"c" const:"1"` + } + type C struct { + ConstB string `json:"b" const:"1"` + ConstC string `json:"c" const:"1"` + } + + payload := `{"b": "1", "c": "1"}` + candidates := makeCandidates(t, payload, A{}, B{}, C{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, C{}, result.Type) +} + +func TestPickBestUnionCandidate_NullPayload(t *testing.T) { + type A struct { + Foo string `json:"foo"` + } + type B *string // nullable type + + payload := `null` + + candidates := makeCandidates(t, payload, A{}, B(nil)) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + _, isB := result.Type.(B) + assertTrue(t, isB, "expected B (nullable type) to win for null payload") +} + +func TestPickBestUnionCandidate_PrimitiveStringTypes(t *testing.T) { + // When both types are strings, first wins on tie + type A = string + type B = string + + payload := `"asdf"` + + candidates := makeCandidates(t, payload, A(""), B("")) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + _, isA := result.Type.(A) + assertTrue(t, isA, "expected A (first type) to win on tie") +} + +func TestPickBestUnionCandidate_NullPointerPrimitives(t *testing.T) { + // A: *string, B: *float64 + // payload: null + // Both can be null, first wins + type A = *string + type B = *float64 + + payload := `null` + + candidates := makeCandidates(t, payload, A(nil), B(nil)) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + _, isA := result.Type.(A) + assertTrue(t, isA, "expected A (*string) to win for null payload (first wins on tie)") +} + +func TestPickBestUnionCandidate_NullPointersMatchOverMissingFields(t *testing.T) { + // A: { a: string } + // B: { b: *string, c: *string } + // payload: { "b": null, "c": null } + // B wins because it has 2 matched fields, A has 0 + type A struct { + A string `json:"a"` + } + type B struct { + B *string `json:"b"` + C *string `json:"c"` + } + + payload := `{"b": null, "c": null}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_MapOfStructs(t *testing.T) { + // A: map[string]{ a: string } + // B: map[string]{ b: string } + // payload: { "x": null, "y": null, "z": { "b": "b" } } + // B wins because it has a matched field in the nested struct + type InnerA struct { + A string `json:"a"` + } + type InnerB struct { + B string `json:"b"` + } + type A = map[string]*InnerA + type B = map[string]*InnerB + + payload := `{"x": null, "y": null, "z": {"b": "b"}}` + + candidates := makeCandidates(t, payload, A(nil), B(nil)) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + _, isB := result.Type.(B) + assertTrue(t, isB, "expected B (map with matching struct field) to win") +} + +func TestPickBestUnionCandidate_NullPointerStructVsString(t *testing.T) { + // A: *{foo: string}, B: *string + // payload: null + type Inner struct { + Foo string `json:"foo"` + } + type A = *Inner + type B = *string + + payload := `null` + + candidates := makeCandidates(t, payload, A(nil), B(nil)) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + _, isA := result.Type.(A) + assertTrue(t, isA, "expected A (*struct) to win for null payload (first wins on tie)") +} + +func TestPickBestUnionCandidate_MapWithNestedStructsVsSimpleField(t *testing.T) { + // A: { a: string } + // B: { b: map[string]{ id: string, name: string } } + // payload: { "a": "", "b": { "foo": { "id": "", "name": "" } } } + // B should win because it has more matched fields (id, name in nested struct) + type Inner struct { + ID string `json:"id"` + Name string `json:"name"` + } + type A struct { + A string `json:"a"` + } + type B struct { + B map[string]Inner `json:"b"` + } + + payload := `{"a": "", "b": {"foo": {"id": "", "name": ""}}}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_AdditionalPropertiesWins(t *testing.T) { + type A struct { + A string `json:"a"` + } + type B struct { + B string `json:"b"` + AdditionalProperties map[string]string `additionalProperties:"true"` + } + + payload := `{"a": "", "b": "", "c": ""}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_StructVsMapOfStrings(t *testing.T) { + // A: map[string]string + // B: { id: string } + // payload: { "id": "", "foo": "" } + // B should win because struct fields are more specific than map + type A = map[string]string + type B struct { + ID string `json:"id"` + } + + payload := `{"id": "", "foo": ""}` + + candidates := makeCandidates(t, payload, A(nil), B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, A{}, result.Type) +} + +func TestPickBestUnionCandidate_AdditionalPropertiesVsExactField(t *testing.T) { + // A: { id: *string, additionalProperties: true } + // B: { foo: string } + // payload: { "foo": "" } + // B should win because it has an exact field match, while A only matches via additionalProperties + type A struct { + ID *string `json:"id"` + AdditionalProperties map[string]string `additionalProperties:"true"` + } + type B struct { + Foo string `json:"foo"` + } + + payload := `{"foo": ""}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_ArrayOfNullableStructs(t *testing.T) { + // A: Array<{ foo: *string } | null> + // B: Array<{ bar: *string } | null> + // payload: [null, null, { "bar": "" }] + // B should win because it has a matching field in the non-null element + type ItemA struct { + Foo *string `json:"foo"` + } + type ItemB struct { + Bar *string `json:"bar"` + } + type A = []*ItemA + type B = []*ItemB + + payload := `[null, null, {"bar": ""}]` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_AnyFieldType(t *testing.T) { + // A: { a: string } + // B: { b: any } + // payload: { "b": "asdf" } + // B should win because it has a matching field + type A struct { + A string `json:"a"` + } + type B struct { + B any `json:"b"` + } + + payload := `{"b": "asdf"}` + candidates := makeCandidates(t, payload, A{}, B{}) + result := PickBestUnionCandidate(candidates, []byte(payload)) + + requireNotNil(t, result) + assertIsType(t, B{}, result.Type) +} + +func TestPickBestUnionCandidate_NonPointerUnionVariants(t *testing.T) { + type Inner struct { + Foo string `json:"foo"` + } + type SliceUnion struct { + AsObject *Inner `union:"member"` + AsList []string `union:"member"` + } + type MapUnion struct { + AsObject *Inner `union:"member"` + AsMap map[string]string `union:"member"` + } + type AnyUnion struct { + AsObject *Inner `union:"member"` + AsAny any `union:"member"` + } + + cases := []struct { + name string + payload string + candidates []UnionCandidate + wantType string + }{ + { + name: "slice variant wins for array payload", + payload: `["a", "b"]`, + candidates: []UnionCandidate{ + {Type: "object", Value: SliceUnion{AsObject: &Inner{}}}, + {Type: "list", Value: SliceUnion{AsList: []string{"a", "b"}}}, + }, + wantType: "list", + }, + { + name: "map variant wins for object payload with no matching struct fields", + payload: `{"k1": "v1", "k2": "v2"}`, + candidates: []UnionCandidate{ + {Type: "object", Value: MapUnion{AsObject: &Inner{}}}, + {Type: "map", Value: MapUnion{AsMap: map[string]string{"k1": "v1", "k2": "v2"}}}, + }, + wantType: "map", + }, + { + name: "any variant wins as catch-all when struct can't fit a scalar", + payload: `"scalar"`, + candidates: []UnionCandidate{ + {Type: "object", Value: AnyUnion{AsObject: &Inner{}}}, + {Type: "any", Value: AnyUnion{AsAny: "scalar"}}, + }, + wantType: "any", + }, + { + name: "structured variant beats any on a matching object payload", + payload: `{"foo": "x"}`, + candidates: []UnionCandidate{ + {Type: "object", Value: AnyUnion{AsObject: &Inner{Foo: "x"}}}, + {Type: "any", Value: AnyUnion{AsAny: map[string]any{"foo": "x"}}}, + }, + wantType: "object", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := PickBestUnionCandidate(tc.candidates, []byte(tc.payload)) + requireNotNil(t, result) + if result.Type != tc.wantType { + t.Errorf("expected type %v, got %v", tc.wantType, result.Type) + } + }) + } +} diff --git a/internal/sdk/sdkinternal/utils/utils.go b/internal/sdk/sdkinternal/utils/utils.go new file mode 100644 index 0000000..004eafa --- /dev/null +++ b/internal/sdk/sdkinternal/utils/utils.go @@ -0,0 +1,372 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package utils + +import ( + "bytes" + "context" + "fmt" + "io" + "math/big" + "net/http" + "reflect" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/optionalnullable" +) + +const ( + queryParamTagKey = "queryParam" + headerParamTagKey = "header" + pathParamTagKey = "pathParam" +) + +var ( + paramRegex = regexp.MustCompile(`({.*?})`) + SerializationMethodToContentType = map[string]string{ + "json": "application/json", + "form": "application/x-www-form-urlencoded", + "multipart": "multipart/form-data", + "raw": "application/octet-stream", + "string": "text/plain", + } +) + +func UnmarshalJsonFromResponseBody(body io.Reader, out interface{}, tag string) error { + data, err := io.ReadAll(body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + if err := UnmarshalJSON(data, out, reflect.StructTag(tag), true, nil); err != nil { + return fmt.Errorf("error unmarshaling json response body: %w", err) + } + + return nil +} + +func UnmarshalJsonFromString(json string, out interface{}, tag string) error { + if err := UnmarshalJSON([]byte(json), out, reflect.StructTag(tag), true, nil); err != nil { + return fmt.Errorf("error unmarshalling json response body: %w", err) + } + + return nil +} + +func ReplaceParameters(stringWithParams string, params map[string]string) string { + if len(params) == 0 { + return stringWithParams + } + + return paramRegex.ReplaceAllStringFunc(stringWithParams, func(match string) string { + match = match[1 : len(match)-1] + return params[match] + }) +} + +func Contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func MatchStatusCodes(expectedCodes []string, statusCode int) bool { + for _, codeStr := range expectedCodes { + code, err := strconv.Atoi(codeStr) + if err == nil { + if code == statusCode { + return true + } + continue + } + + codeRange, err := strconv.Atoi(string(codeStr[0])) + if err != nil { + continue + } + + if statusCode >= (codeRange*100) && statusCode < ((codeRange+1)*100) { + return true + } + } + + return false +} + +func AsSecuritySource(security interface{}) func(context.Context) (interface{}, error) { + return func(context.Context) (interface{}, error) { + return security, nil + } +} + +func parseConstTag(field reflect.StructField) *string { + value := field.Tag.Get("const") + + if value == "" { + return nil + } + + return &value +} + +func parseDefaultTag(field reflect.StructField) *string { + value := field.Tag.Get("default") + + if value == "" { + return nil + } + + return &value +} + +func parseStructTag(tagKey string, field reflect.StructField) map[string]string { + tag := field.Tag.Get(tagKey) + if tag == "" { + return nil + } + + values := map[string]string{} + + options := strings.Split(tag, ",") + for _, optionConf := range options { + parts := strings.Split(optionConf, "=") + + switch len(parts) { + case 1: + // flag option + parts = append(parts, "true") + case 2: + // key=value option + default: + // invalid option + continue + } + + values[parts[0]] = parts[1] + } + + return values +} + +func parseParamTag(tagKey string, field reflect.StructField, defaultStyle string, defaultExplode bool) *paramTag { + // example `{tagKey}:"style=simple,explode=false,name=apiID"` + // example `{tagKey}:"inline"` + values := parseStructTag(tagKey, field) + if values == nil { + return nil + } + + tag := ¶mTag{ + Style: defaultStyle, + Explode: defaultExplode, + ParamName: strings.ToLower(field.Name), + } + + for k, v := range values { + switch k { + case "inline": + tag.Inline = v == "true" + case "style": + tag.Style = v + case "explode": + tag.Explode = v == "true" + case "name": + tag.ParamName = v + case "serialization": + tag.Serialization = v + } + } + + return tag +} + +func valToString(val interface{}) string { + switch v := val.(type) { + case time.Time: + return v.Format(time.RFC3339Nano) + case big.Int: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} + +func populateFromGlobals(fieldType reflect.StructField, valType reflect.Value, paramType string, globals interface{}) (reflect.StructField, reflect.Value, bool) { + if globals == nil { + return fieldType, valType, false + } + + globalsStruct := reflect.TypeOf(globals) + globalsStructVal := reflect.ValueOf(globals) + + globalsField, found := globalsStruct.FieldByName(fieldType.Name) + if !found { + return fieldType, valType, false + } + + if fieldType.Type.Kind() != reflect.Ptr || !valType.IsNil() { + return fieldType, valType, true + } + + globalsVal := globalsStructVal.FieldByName(fieldType.Name) + + if !globalsVal.IsValid() { + return fieldType, valType, false + } + + switch paramType { + case queryParamTagKey: + qpTag := parseQueryParamTag(globalsField) + if qpTag == nil { + return fieldType, valType, false + } + default: + tag := parseParamTag(paramType, fieldType, "simple", false) + if tag == nil { + return fieldType, valType, false + } + } + + return globalsField, globalsVal, true +} + +func isNil(typ reflect.Type, val reflect.Value) bool { + // `reflect.TypeOf(nil) == nil` so calling typ.Kind() will cause a nil pointer + // dereference panic. Catch it and return early. + // https://github.com/golang/go/issues/51649 + // https://github.com/golang/go/issues/54208 + if typ == nil { + return true + } + + if typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Map || typ.Kind() == reflect.Slice || typ.Kind() == reflect.Interface { + return val.IsNil() + } + + return false +} + +func unwrapOptionalNullable(val reflect.Value) (reflect.Value, bool) { + if val.Kind() == reflect.Map && val.IsNil() && val.CanInterface() { + if _, isWrapper := val.Interface().(optionalnullable.OptionalNullableInterface); isWrapper { + return val, false + } + } + + nullableValue, ok := optionalnullable.AsOptionalNullable(val) + if !ok { + return val, true + } + + inner, isSet := nullableValue.GetUntyped() + if !isSet || inner == nil { + return val, false + } + + val = reflect.ValueOf(inner) + if isNil(val.Type(), val) { + return val, false + } + + if val.Kind() == reflect.Pointer { + val = val.Elem() + } + + return val, true +} + +func isEmptyContainer(typ reflect.Type, val reflect.Value) bool { + if isNil(typ, val) { + return true + } + + switch typ.Kind() { + case reflect.Slice, reflect.Array: + return val.Len() == 0 + case reflect.Map: + return val.Len() == 0 + default: + return false + } +} + +func contains(arr []string, str string) bool { + for _, a := range arr { + if a == str { + return true + } + } + return false +} + +func DrainBody(res *http.Response) { + io.Copy(io.Discard, res.Body) + res.Body.Close() + res.Body = io.NopCloser(bytes.NewReader(nil)) +} + +func ConsumeRawBody(res *http.Response) ([]byte, error) { + defer res.Body.Close() + + rawBody, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + res.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + + return rawBody, nil +} + +type bodyWithCancel struct { + io.ReadCloser + cancel context.CancelFunc + once sync.Once +} + +func (b *bodyWithCancel) Read(p []byte) (int, error) { + n, err := b.ReadCloser.Read(p) + if err != nil { + b.release() + } + return n, err +} + +func (b *bodyWithCancel) Close() error { + err := b.ReadCloser.Close() + b.release() + return err +} + +func (b *bodyWithCancel) release() { + b.once.Do(b.cancel) +} + +// BodyWithCancel returns body wrapped so that cancel runs once reading ends or +// the body is closed. A nil cancel returns body unchanged. +func BodyWithCancel(body io.ReadCloser, cancel context.CancelFunc) io.ReadCloser { + if cancel == nil { + return body + } + return &bodyWithCancel{ReadCloser: body, cancel: cancel} +} diff --git a/internal/sdk/triggers.go b/internal/sdk/triggers.go new file mode 100644 index 0000000..7baf551 --- /dev/null +++ b/internal/sdk/triggers.go @@ -0,0 +1,1354 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/triggers" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/spyzhov/ajson" +) + +// Triggers - Schedule and manage cron triggers that run managed agents +type Triggers struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newTriggers(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Triggers { + return &Triggers{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// List triggers for a project +// Lists triggers for a project. +func (s *Triggers) List(ctx context.Context, request *operations.ListTriggersRequest, opts ...operations.Option) (*operations.ListTriggersResponse, error) { + globals := operations.ListTriggersGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/triggers", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ListTriggers", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + paginationCtx := ctx + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ListTriggersResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + res.Next = func() (*operations.ListTriggersResponse, error) { + if request == nil { + request = &operations.ListTriggersRequest{} + } + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return nil, err + } + nC, err := ajson.Eval(b, "$.next_page_token") + if err != nil { + return nil, err + } + var nCVal string + + if nC.IsNumeric() { + numVal, err := nC.GetNumeric() + if err != nil { + return nil, err + } + // GetNumeric returns as float64 so convert to the appropriate type. + nCVal = strconv.FormatFloat(numVal, 'f', 0, 64) + } else { + val, err := nC.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + nCVal = val.(string) + if strings.TrimSpace(nCVal) == "" { + return nil, nil + } + } + request.PageToken = &nCVal + + return s.List( + paginationCtx, + request, + opts..., + ) + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out triggers.ListTriggersResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ListTriggersResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Delete a trigger by ID +// Deletes a trigger. +func (s *Triggers) Delete(ctx context.Context, request operations.DeleteTriggerRequest, opts ...operations.Option) (*operations.DeleteTriggerResponse, error) { + globals := operations.DeleteTriggerGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/triggers/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "DeleteTrigger", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteTriggerResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Empty + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Empty = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Get a trigger by ID +// Gets details of a single trigger. +func (s *Triggers) Get(ctx context.Context, request operations.GetTriggerRequest, opts ...operations.Option) (*operations.GetTriggerResponse, error) { + globals := operations.GetTriggerGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/triggers/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "GetTrigger", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetTriggerResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out triggers.Trigger + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Trigger = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Update a trigger by ID +// Updates a trigger. +func (s *Triggers) Update(ctx context.Context, request operations.UpdateTriggerRequest, opts ...operations.Option) (*operations.UpdateTriggerResponse, error) { + globals := operations.UpdateTriggerGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/triggers/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "UpdateTrigger", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "PATCH", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.UpdateTriggerResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out triggers.Trigger + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Trigger = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// ListExecutions - List executions for a trigger +// Lists executions for a trigger. +func (s *Triggers) ListExecutions(ctx context.Context, request operations.ListTriggerExecutionsRequest, opts ...operations.Option) (*operations.ListTriggerExecutionsResponse, error) { + globals := operations.ListTriggerExecutionsGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/triggers/{trigger_id}/executions", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ListTriggerExecutions", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + paginationCtx := ctx + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ListTriggerExecutionsResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + res.Next = func() (*operations.ListTriggerExecutionsResponse, error) { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return nil, err + } + nC, err := ajson.Eval(b, "$.next_page_token") + if err != nil { + return nil, err + } + var nCVal string + + if nC.IsNumeric() { + numVal, err := nC.GetNumeric() + if err != nil { + return nil, err + } + // GetNumeric returns as float64 so convert to the appropriate type. + nCVal = strconv.FormatFloat(numVal, 'f', 0, 64) + } else { + val, err := nC.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + nCVal = val.(string) + if strings.TrimSpace(nCVal) == "" { + return nil, nil + } + } + request.PageToken = &nCVal + + return s.ListExecutions( + paginationCtx, + request, + opts..., + ) + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out triggers.ListTriggerExecutionsResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ListTriggerExecutionsResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// Run a trigger immediately +// Runs a trigger immediately. +func (s *Triggers) Run(ctx context.Context, request operations.RunTriggerRequest, opts ...operations.Option) (*operations.RunTriggerResponse, error) { + globals := operations.RunTriggerGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/triggers/{trigger_id}/executions", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "RunTrigger", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.RunTriggerResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out triggers.TriggerExecution + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.TriggerExecution = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} diff --git a/internal/sdk/types/bigint.go b/internal/sdk/types/bigint.go new file mode 100644 index 0000000..904757a --- /dev/null +++ b/internal/sdk/types/bigint.go @@ -0,0 +1,35 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package types + +import ( + "fmt" + "math/big" +) + +// MustNewBigIntFromString returns an instance of big.Int from a string +// The string is assumed to be base 10 and if it is not a valid big.Int +// then the function panics. +// Avoid using this function in production code. +func MustNewBigIntFromString(s string) *big.Int { + i, ok := new(big.Int).SetString(s, 10) + if !ok { + panic(fmt.Errorf("failed to parse string as big.Int")) + } + + return i +} diff --git a/internal/sdk/types/date.go b/internal/sdk/types/date.go new file mode 100644 index 0000000..c181d08 --- /dev/null +++ b/internal/sdk/types/date.go @@ -0,0 +1,104 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package types + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// Date is a wrapper around time.Time that allows for JSON marshaling a date string formatted as "2006-01-02". +type Date struct { + time.Time +} + +var ( + _ json.Marshaler = &Date{} + _ json.Unmarshaler = &Date{} + _ fmt.Stringer = &Date{} +) + +// NewDate returns an instance of Date from a time.Time. +func NewDate(t time.Time) *Date { + d := DateFromTime(t) + return &d +} + +// DateFromTime returns a Date from a time.Time. +func DateFromTime(t time.Time) Date { + return Date{t} +} + +// NewDateFromString returns an instance of Date from a string formatted as "2006-01-02". +func NewDateFromString(str string) (*Date, error) { + d, err := DateFromString(str) + if err != nil { + return nil, err + } + + return &d, nil +} + +// DateFromString returns a Date from a string formatted as "2006-01-02". +func DateFromString(str string) (Date, error) { + var d Date + var err error + + d.Time, err = time.Parse("2006-01-02", str) + return d, err +} + +// MustNewDateFromString returns an instance of Date from a string formatted as "2006-01-02" or panics. +// Avoid using this function in production code. +func MustNewDateFromString(str string) *Date { + d := MustDateFromString(str) + return &d +} + +// MustDateFromString returns a Date from a string formatted as "2006-01-02" or panics. +// Avoid using this function in production code. +func MustDateFromString(str string) Date { + d, err := DateFromString(str) + if err != nil { + panic(err) + } + return d +} + +func (d Date) GetTime() time.Time { + return d.Time +} + +func (d Date) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, d.Time.Format("2006-01-02"))), nil +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var err error + + str := string(data) + str = strings.Trim(str, `"`) + + d.Time, err = time.Parse("2006-01-02", str) + return err +} + +func (d Date) String() string { + return d.Time.Format("2006-01-02") +} diff --git a/internal/sdk/types/datetime.go b/internal/sdk/types/datetime.go new file mode 100644 index 0000000..3c78b73 --- /dev/null +++ b/internal/sdk/types/datetime.go @@ -0,0 +1,37 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package types + +import "time" + +// MustTimeFromString returns a time.Time from a string formatted as "2006-01-02T15:04:05Z07:00" or panics. +// Avoid using this function in production code. +func MustTimeFromString(str string) time.Time { + t, err := time.Parse(time.RFC3339, str) + if err != nil { + panic(err) + } + + return t +} + +// MustNewTimeFromString returns an instance of time.Time from a string formatted as "2006-01-02T15:04:05Z07:00" or panics. +// Avoid using this function in production code. +func MustNewTimeFromString(str string) *time.Time { + t := MustTimeFromString(str) + return &t +} diff --git a/internal/sdk/types/pointers.go b/internal/sdk/types/pointers.go new file mode 100644 index 0000000..0c6d97b --- /dev/null +++ b/internal/sdk/types/pointers.go @@ -0,0 +1,25 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package types + +func String(s string) *string { return &s } +func Bool(b bool) *bool { return &b } +func Int(i int) *int { return &i } +func Int64(i int64) *int64 { return &i } +func Float32(f float32) *float32 { return &f } +func Float64(f float64) *float64 { return &f } +func Pointer[T any](v T) *T { return &v } diff --git a/internal/sdk/types/stream/stream.go b/internal/sdk/types/stream/stream.go new file mode 100644 index 0000000..11c49b5 --- /dev/null +++ b/internal/sdk/types/stream/stream.go @@ -0,0 +1,345 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package stream + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "io" + "regexp" + "strconv" + "strings" + "sync" + "sync/atomic" +) + +type ServerEvent struct { + ID *string `json:"id,omitempty"` + Event *string `json:"event,omitempty"` + Data json.RawMessage `json:"data,omitempty"` + Retry *int64 `json:"retry,omitempty"` +} + +var ( + boundary = regexp.MustCompile(`\r\n\r\n|\r\n\r|\r\n\n|\r\r\n|\n\r\n|\r\r|\n\r|\n\n`) + lineEnding = regexp.MustCompile(`\r\n|\r|\n`) + bom = "\uFEFF" +) + +// maxBoundaryLen is the length of the longest message boundary (\r\n\r\n). +const maxBoundaryLen = 4 + +// maxEventSize bounds a single server-sent event. The scanner buffer grows on +// demand, so this caps pathological streams without reserving memory up front. +const maxEventSize = 1 << 30 + +// newServerEventSplitter returns a stateful bufio.SplitFunc that only re-scans +// the trailing maxBoundaryLen-1 bytes across reads. That keeps large events +// linear to parse while still finding boundaries split across two chunks. +func newServerEventSplitter() bufio.SplitFunc { + scanned := 0 + return func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + start := scanned - (maxBoundaryLen - 1) + if start < 0 { + start = 0 + } + + if result := boundary.FindIndex(data[start:]); result != nil { + scanned = 0 + return start + result[1], data[:start+result[0]], nil + } + + if atEOF { + scanned = 0 + return len(data), bytes.TrimRight(data, "\r\n"), nil + } + + scanned = len(data) + return 0, nil, nil + } +} + +type EventType interface { + GetEventEncoding(event string) (string, error) +} + +type EventStream[T any] struct { + r io.ReadCloser + scanner *bufio.Scanner + unmarshaller func(se []byte) (T, error) + sentinel string + ctx context.Context + cancel context.CancelFunc + releaseOnce sync.Once + dataRequired bool + + finished atomic.Bool + first bool + err error + val *T + eventID *string +} + +func NewEventStream[T any]( + ctx context.Context, + source io.Reader, + unmarshaller func(se []byte) (T, error), + sentinel string, + opts ...func(*EventStream[T]), +) *EventStream[T] { + scanner := bufio.NewScanner(source) + scanner.Buffer(nil, maxEventSize) + scanner.Split(newServerEventSplitter()) + + var src io.ReadCloser + if s, ok := source.(io.ReadCloser); ok { + src = s + } else { + src = io.NopCloser(source) + } + + if ctx == nil { + ctx = context.Background() + } + + es := &EventStream[T]{ + r: src, + scanner: scanner, + unmarshaller: unmarshaller, + sentinel: sentinel, + ctx: ctx, + first: true, + dataRequired: true, + } + for _, opt := range opts { + opt(es) + } + return es +} + +func WithDataRequired[T any](dataRequired bool) func(*EventStream[T]) { + return func(es *EventStream[T]) { + es.dataRequired = dataRequired + } +} + +// WithCancel hands ownership of a context cancel function (typically the +// request timeout's) to the stream. The deadline keeps bounding the whole +// stream; the stream releases the context when it ends or is closed instead +// of the caller cancelling it before iteration. A nil cancel is ignored. +func WithCancel[T any](cancel context.CancelFunc) func(*EventStream[T]) { + return func(es *EventStream[T]) { + if cancel != nil { + es.cancel = cancel + } + } +} + +// release cancels the owned context, once. +func (es *EventStream[T]) release() { + es.releaseOnce.Do(func() { + if es.cancel != nil { + es.cancel() + } + }) +} + +// Next waits for the next event from a stream which will be available +// through the Value() method. It returns false when the stream is done or +// an error occurred. After this method returns false, the Err method is used +// to check for any errors that occurred while parsing the stream. +func (es *EventStream[T]) Next() bool { + if es.err != nil || es.finished.Load() { + return false + } + + for { + // Re-checked every iteration: comment-only and data-less keepalive + // frames loop here without publishing, and the retained operation + // timeout must still be able to stop the stream. + select { + case <-es.ctx.Done(): + es.err = es.ctx.Err() + es.release() + return false + default: + } + + if !es.scanner.Scan() { + es.err = es.scanner.Err() + es.finished.Store(true) + es.release() + return false + } + + b := es.scanner.Bytes() + content := string(b) + if es.first { + es.first = false + content = strings.TrimPrefix(content, bom) + } + + var event ServerEvent + lines := lineEnding.Split(content, -1) + publish := false + data := "" + for _, line := range lines { + if line == "" { + continue + } + + delim := strings.Index(line, ":") + if delim == 0 { + continue + } + + var field, value string + if delim > 0 { + field = line[:delim] + value = line[delim+1:] + value = strings.TrimPrefix(value, " ") + } else { + field = line + value = "" + } + + switch field { + case "id": + publish = true + if !strings.Contains(value, "\x00") { + es.eventID = &value + } + case "event": + publish = true + event.Event = &value + case "retry": + retry, err := strconv.ParseInt(value, 10, 64) + if err == nil { + publish = true + event.Retry = &retry + } + case "data": + publish = true + data += value + "\n" + } + } + + // Skip comment-only or empty blocks (e.g. SSE keepalive heartbeats) + if !publish { + continue + } + + // Skip events with no data lines when data is required + if data == "" && es.dataRequired { + continue + } + + event.ID = es.eventID + + if es.sentinel != "" && data == es.sentinel+"\n" { + es.finished.Store(true) + es.release() + return false + } + + if len(data) > 0 { + data = data[:len(data)-1] + } + + encoding := "application/json" + + var t T + if et, ok := any(t).(EventType); ok { + ev := "" + if event.Event != nil { + ev = *event.Event + } + encoding, _ = et.GetEventEncoding(ev) + } else { + var a interface{} + if err := json.Unmarshal([]byte(data), &a); err != nil { + encoding = "string" + } + } + + // "auto" means the data field is a mixed union (JSON + plain-text variants). + // Probe the actual data to decide. + if encoding == "auto" { + var a interface{} + if err := json.Unmarshal([]byte(data), &a); err != nil { + encoding = "string" + } else { + encoding = "application/json" + } + } + + if encoding == "string" { + jsonData, err := json.Marshal(data) + if err != nil { + es.err = err + es.release() + return false + } + event.Data = jsonData + } else { + event.Data = []byte(data) + } + + e, err := json.Marshal(event) + if err != nil { + es.err = err + es.release() + return false + } + + parsedEvent, err := es.unmarshaller(e) + if err != nil { + es.err = err + es.release() + return false + } + + es.val = &parsedEvent + + return true + } +} + +// Value returns the most recent event that was generated from a call to Next +func (es *EventStream[T]) Value() *T { + return es.val +} + +// Err returns the first non-EOF error that was encountered +func (es *EventStream[T]) Err() error { + return es.err +} + +// Close will release underlying resources held by an event stream. It must +// always be called. +func (es *EventStream[T]) Close() error { + es.finished.Store(true) + err := es.r.Close() + es.release() + return err +} diff --git a/internal/sdk/webhooks.go b/internal/sdk/webhooks.go new file mode 100644 index 0000000..a1a673d --- /dev/null +++ b/internal/sdk/webhooks.go @@ -0,0 +1,1379 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package sdk + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/components" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/interactions" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/operations" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/sdkerrors" + "github.com/google-gemini/gemini-api-cli/internal/sdk/models/webhooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/retry" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/config" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/hooks" + "github.com/google-gemini/gemini-api-cli/internal/sdk/sdkinternal/utils" + "github.com/spyzhov/ajson" +) + +// Webhooks - Manage webhook endpoints and signing secrets for event delivery +type Webhooks struct { + rootSDK *GeminiAPI + sdkConfiguration config.SDKConfiguration + hooks *hooks.Hooks +} + +func newWebhooks(rootSDK *GeminiAPI, sdkConfig config.SDKConfiguration, hooks *hooks.Hooks) *Webhooks { + return &Webhooks{ + rootSDK: rootSDK, + sdkConfiguration: sdkConfig, + hooks: hooks, + } +} + +// List webhook endpoints +// Lists all Webhooks. +func (s *Webhooks) List(ctx context.Context, request *operations.ListWebhooksRequest, opts ...operations.Option) (*operations.ListWebhooksResponse, error) { + globals := operations.ListWebhooksGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "ListWebhooks", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + paginationCtx := ctx + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.ListWebhooksResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + res.Next = func() (*operations.ListWebhooksResponse, error) { + if request == nil { + request = &operations.ListWebhooksRequest{} + } + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + b, err := ajson.Unmarshal(rawBody) + if err != nil { + return nil, err + } + nC, err := ajson.Eval(b, "$.next_page_token") + if err != nil { + return nil, err + } + var nCVal string + + if nC.IsNumeric() { + numVal, err := nC.GetNumeric() + if err != nil { + return nil, err + } + // GetNumeric returns as float64 so convert to the appropriate type. + nCVal = strconv.FormatFloat(numVal, 'f', 0, 64) + } else { + val, err := nC.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + nCVal = val.(string) + if strings.TrimSpace(nCVal) == "" { + return nil, nil + } + } + request.PageToken = &nCVal + + return s.List( + paginationCtx, + request, + opts..., + ) + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out webhooks.WebhookListResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.WebhookListResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Create a webhook endpoint +// Creates a new Webhook. +func (s *Webhooks) Create(ctx context.Context, request operations.CreateWebhookRequest, opts ...operations.Option) (*operations.CreateWebhookResponse, error) { + globals := operations.CreateWebhookGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "CreateWebhook", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CreateWebhookResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out webhooks.Webhook + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Webhook = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Delete a webhook by ID +// Deletes a Webhook. +func (s *Webhooks) Delete(ctx context.Context, request operations.DeleteWebhookRequest, opts ...operations.Option) (*operations.DeleteWebhookResponse, error) { + globals := operations.DeleteWebhookGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "DeleteWebhook", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteWebhookResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out interactions.Empty + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Empty = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Get a webhook by ID +// Gets a specific Webhook. +func (s *Webhooks) Get(ctx context.Context, request operations.GetWebhookRequest, opts ...operations.Option) (*operations.GetWebhookResponse, error) { + globals := operations.GetWebhookGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "GetWebhook", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } else { + retryConfig = &retry.Config{ + Strategy: "attempt-count-backoff", Backoff: &retry.BackoffStrategy{ + InitialInterval: 500, + MaxInterval: 8000, + Exponent: 2, + MaxElapsedTime: 30000, + }, + RetryConnectionErrors: true, + MaxRetries: func(i int) *int { return &i }(4), + } + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "408", + "429", + "5XX", + }, + }, func(attempt int) (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + + if err != nil { + return nil, err + } + + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetWebhookResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out webhooks.Webhook + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Webhook = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Update a webhook by ID +// Updates an existing Webhook. +func (s *Webhooks) Update(ctx context.Context, request operations.UpdateWebhookRequest, opts ...operations.Option) (*operations.UpdateWebhookResponse, error) { + globals := operations.UpdateWebhookGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks/{id}", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "UpdateWebhook", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "PATCH", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateQueryParams(ctx, req, request, globals, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.UpdateWebhookResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out webhooks.Webhook + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Webhook = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// Ping - Send a ping event to a webhook +// Sends a ping event to a Webhook. +func (s *Webhooks) Ping(ctx context.Context, request operations.PingWebhookRequest, opts ...operations.Option) (*operations.PingWebhookResponse, error) { + globals := operations.PingWebhookGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks/{id}:ping", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "PingWebhook", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.PingWebhookResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out webhooks.WebhookPingResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.WebhookPingResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// RotateSigningSecret - Rotate the signing secret for a webhook +// Generates a new signing secret for a Webhook. +func (s *Webhooks) RotateSigningSecret(ctx context.Context, request operations.RotateSigningSecretRequest, opts ...operations.Option) (*operations.RotateSigningSecretResponse, error) { + globals := operations.RotateSigningSecretGlobals{ + APIVersion: s.sdkConfiguration.Globals.APIVersion, + APIRevision: s.sdkConfiguration.Globals.APIRevision, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + operations.SupportedOptionSkipDeserialization, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/{api_version}/webhooks/{id}:rotateSigningSecret", request, globals) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s.rootSDK, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "RotateSigningSecret", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "Body", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + var streamCancel context.CancelFunc + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + streamCancel = cancel + defer func() { + if streamCancel != nil { + streamCancel() + } + }() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + utils.PopulateHeaders(ctx, req, request, globals) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.RotateSigningSecretResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + if o.SkipDeserialization != nil && *o.SkipDeserialization { + httpRes.Body = utils.BodyWithCancel(httpRes.Body, streamCancel) + streamCancel = nil + } else { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out webhooks.WebhookRotateSigningSecretResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.WebhookRotateSigningSecretResponse = &out + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKDefaultError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} diff --git a/internal/testclient/testclient.go b/internal/testclient/testclient.go new file mode 100644 index 0000000..3a51d1f --- /dev/null +++ b/internal/testclient/testclient.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package testclient + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// TestHTTPClient wraps an HTTP client and injects test headers when env vars are set. +type TestHTTPClient struct { + client *http.Client + testName string + instanceID string +} + +// NewTestHTTPClient creates a new test HTTP client if test env vars are set. +// Returns nil if not in test mode. +func NewTestHTTPClient() *TestHTTPClient { + testName := os.Getenv("SPEAKEASY_TEST_NAME") + if testName == "" { + return nil + } + + instanceID := os.Getenv("SPEAKEASY_TEST_INSTANCE_ID") + + return &TestHTTPClient{ + client: &http.Client{Timeout: 60 * time.Second}, + testName: testName, + instanceID: instanceID, + } +} + +// Do executes the HTTP request, injecting test headers. +// Intercepts OAuth2 token exchange requests and returns a mock token response +// so tests don't require a real token endpoint on the mock server. +func (c *TestHTTPClient) Do(req *http.Request) (*http.Response, error) { + if isOAuth2TokenRequest(req) { + return mockTokenResponse(), nil + } + req.Header.Set("x-speakeasy-test-name", c.testName) + if c.instanceID != "" { + req.Header.Set("x-speakeasy-test-instance-id", c.instanceID) + } + return c.client.Do(req) +} + +// isOAuth2TokenRequest detects OAuth2 token exchange requests by checking for +// grant_type in the form-encoded body. +func isOAuth2TokenRequest(req *http.Request) bool { + if req.Method != http.MethodPost { + return false + } + contentType := req.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + return false + } + body, err := io.ReadAll(req.Body) + if err != nil { + return false + } + req.Body = io.NopCloser(bytes.NewBuffer(body)) + values, err := url.ParseQuery(string(body)) + if err != nil { + return false + } + grantType := values.Get("grant_type") + // Only intercept client_credentials grants. Password grants must reach the + // real token endpoint because the API validates the resulting JWT. + return grantType == "client_credentials" +} + +// mockTokenResponse returns a valid OAuth2 token response for testing. +func mockTokenResponse() *http.Response { + tokenBody := map[string]interface{}{ + "access_token": "test_mock_access_token", + "token_type": "Bearer", + "expires_in": 3600, + } + body, _ := json.Marshal(tokenBody) + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + } +} diff --git a/internal/usage/bodyschemas.go b/internal/usage/bodyschemas.go new file mode 100644 index 0000000..b8848ab --- /dev/null +++ b/internal/usage/bodyschemas.go @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package usage + +import ( + "fmt" + "io" +) + +var bodySchemas = map[string]string{ + "CreateAgent": "{\"$defs\":{\"Agent\":{\"description\":\"An agent definition for the CreateAgent API.\\nThis message is the target for annotation-parser-based JSON parsing.\\nNew format:\\n {\\n \\\"id\\\": \\\"customer-sentinel\\\",\\n \\\"base_agent\\\": \\\"\\\",\\n \\\"system_instruction\\\": \\\"...\\\",\\n \\\"base_environment\\\": { \\\"type\\\": \\\"remote\\\", \\\"sources\\\": [...] },\\n \\\"tools\\\": [ {\\\"type\\\": \\\"code_execution\\\"} ]\\n }\",\"properties\":{\"agent_config\":{\"description\":\"Configuration parameters for the agent.\",\"oneOf\":[{\"$ref\":\"#/$defs/AntigravityAgentConfig\"}]},\"base_agent\":{\"description\":\"The base agent to extend.\",\"type\":\"string\"},\"base_environment\":{\"description\":\"The environment configuration for the agent.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentConfig\"},{\"type\":\"string\"}]},\"description\":{\"description\":\"Agent description for developers to quickly read and understand.\",\"type\":\"string\"},\"id\":{\"description\":\"The unique identifier for the agent.\",\"type\":\"string\"},\"system_instruction\":{\"description\":\"System instruction for the agent.\",\"type\":\"string\"},\"tools\":{\"description\":\"The tools available to the agent.\",\"items\":{\"$ref\":\"#/$defs/AgentTool\"},\"type\":\"array\"\x7d\x7d,\"required\":[\"base_agent\",\"id\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"agents\",\"name\":\"Agent\"}],\"x-speakeasy-model-namespace\":\"agents\"},\"AgentTool\":{\"description\":\"A tool that the agent can use.\",\"oneOf\":[{\"$ref\":\"#/$defs/CodeExecution\"},{\"$ref\":\"#/$defs/Function\"},{\"$ref\":\"#/$defs/GoogleSearch\"},{\"$ref\":\"#/$defs/McpServer\"},{\"$ref\":\"#/$defs/UrlContext\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"agents\",\"name\":\"AgentTool\"}],\"x-speakeasy-model-namespace\":\"agents\"},\"AllowedTools\":{\"description\":\"The configuration for allowed tools.\",\"properties\":{\"mode\":{\"$ref\":\"#/$defs/ToolChoiceType\",\"description\":\"The mode of the tool choice.\"},\"tools\":{\"description\":\"The names of the allowed tools.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"AllowedTools\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"AllowlistEntry\":{\"description\":\"A single domain allowlist rule with optional header injection.\",\"properties\":{\"credential\":{\"description\":\"Optional. Reference to a server-managed Credential resource by ID.\",\"type\":\"string\"},\"domain\":{\"description\":\"Domain to allow outbound requests to. Supports wildcards (e.g. '*.googleapis.com'). Use '*' to allow all domains.\",\"type\":\"string\"},\"transform\":{\"description\":\"Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically.\",\"oneOf\":[{\"description\":\"A list of headers to inject.\",\"items\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"A single header to inject.\",\"type\":\"object\"},\"type\":\"array\"},{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"A single header injection mapping, e.g., {\\\"Authorization\\\": \\\"Bearer token\\\"}.\",\"type\":\"object\"}]\x7d\x7d,\"required\":[\"domain\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"AntigravityAgentConfig\":{\"description\":\"Configuration for the Antigravity agent runtime.\\nProvides server-side control over the agent's execution environment\\nand tool configuration.\",\"properties\":{\"max_total_tokens\":{\"description\":\"Max total tokens for the agent run.\",\"format\":\"int64\",\"type\":\"string\"},\"model\":{\"description\":\"The model to use for agent reasoning.\",\"type\":\"string\"},\"type\":{\"const\":\"antigravity\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"AntigravityAgentConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"CodeExecution\":{\"description\":\"A tool that can be used by the model to execute code.\",\"properties\":{\"type\":{\"const\":\"code_execution\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"code_execution\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"code_execution\\\"\\n }],\\n \\\"input\\\": \\\"Calculate the first 10 Fibonacci numbers\\\"\\n }'\\n\"},{\"label\":\"code_execution\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\\"type\\\": \\\"code_execution\\\"}],\\n input=\\\"Calculate the first 10 Fibonacci numbers\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"code_execution\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{ type: 'code_execution' }],\\n input: 'Calculate the first 10 Fibonacci numbers'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"code_execution\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CodeExecution;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(new CodeExecution()))\\n .input(InteractionsInput.of(\\\"Calculate the first 10 Fibonacci numbers\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"EnvVar\":{\"description\":\"An environment variable to set in the execution environment.\",\"properties\":{\"credential\":{\"description\":\"Optional reference to a server-managed Credential resource by ID.\",\"type\":\"string\"},\"value\":{\"description\":\"Direct string value for plain environment variables.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"EnvironmentConfig\":{\"description\":\"Configuration for a custom environment.\",\"examples\":[{\"inline_sources\":{\"summary\":\"Inline Sources\",\"value\":{\"sources\":[{\"content\":\"You are a data analyst. Always include visualizations and export results as PDF.\",\"target\":\".agents/AGENTS.md\",\"type\":\"inline\"},{\"content\":\"---\\nname: slide-maker\\ndescription: Create HTML slide decks\\n---\\n# Slide Maker\\n\\nWhen asked to create a presentation:\\n1. Analyze the input data\\n2. Create an HTML slide deck with reveal.js\\n3. Save to /workspace/output/slides.html\",\"target\":\".agents/skills/slide-maker/SKILL.md\",\"type\":\"inline\"}],\"type\":\"remote\"\x7d\x7d},{\"external_sources\":{\"summary\":\"External Sources\",\"value\":{\"sources\":[{\"source\":\"https://github.com/my-org/my-skills.git\",\"target\":\".agents/skills\",\"type\":\"repository\"},{\"source\":\"gs://my-bucket/my-folder\",\"target\":\"/workspace/data\",\"type\":\"gcs\"}],\"type\":\"remote\"\x7d\x7d},{\"network_allowlist\":{\"summary\":\"Network Allowlist\",\"value\":{\"network\":{\"allowlist\":[{\"domain\":\"pypi.org\"},{\"domain\":\"*.github.com\"}]},\"type\":\"remote\"\x7d\x7d},{\"proxy_credentials\":{\"summary\":\"Proxy Credentials\",\"value\":{\"network\":{\"allowlist\":[{\"domain\":\"api.github.com\",\"transform\":{\"Authorization\":\"Bearer YOUR_GITHUB_TOKEN\"\x7d\x7d]},\"type\":\"remote\"\x7d\x7d}],\"properties\":{\"env\":{\"description\":\"Environment variables to set in the sandbox environment.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvVar\"},{\"additionalProperties\":{\"$ref\":\"#/$defs/EnvVar\"},\"type\":\"object\"}]},\"environment_id\":{\"description\":\"Optional. The environment ID for the interaction. If specified, the request will\\nupdate the existing environment instead of creating a new one.\",\"type\":\"string\"},\"network\":{\"description\":\"Network configuration for the environment.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentNetworkEgressAllowlist\"},{\"enum\":[\"disabled\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"All network egress is blocked.\"]}]},\"sources\":{\"items\":{\"$ref\":\"#/$defs/Source\"},\"type\":\"array\"},\"type\":{\"const\":\"remote\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Environment\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"Environment\"},\"EnvironmentNetworkEgressAllowlist\":{\"description\":\"Outbound networking configuration for the sandbox. Accepts an object with an 'allowlist' array to restrict traffic, or the string 'disabled' to turn off all network access. Omit entirely to allow all outbound traffic with no header injection.\",\"example\":{\"allowlist\":[{\"domain\":\"github.com\",\"transform\":[{\"Authorization\":\"Bearer your-token\"}]},{\"domain\":\"*.googleapis.com\"}]},\"oneOf\":[{\"description\":\"Outbound networking configuration for the sandbox. When specified, restricts which external domains the sandbox can reach. Omit entirely to allow all outbound traffic with no header injection.\",\"example\":{\"allowlist\":[{\"domain\":\"pypi.org\"},{\"domain\":\"*.github.com\"}]},\"properties\":{\"allowlist\":{\"description\":\"List of allowed outbound domains. Only requests to listed domains are permitted. Use [{'domain': '*'}] to allow all domains while still injecting headers on specific ones.\",\"items\":{\"$ref\":\"#/$defs/AllowlistEntry\"},\"type\":\"array\"\x7d\x7d,\"title\":\"Allowlist\",\"type\":\"object\"},{\"description\":\"Turns all network off.\",\"enum\":[\"disabled\"],\"example\":\"disabled\",\"title\":\"Disabled\",\"type\":\"string\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"Function\":{\"description\":\"A tool that can be used by the model.\",\"properties\":{\"description\":{\"description\":\"A description of the function.\",\"type\":\"string\"},\"name\":{\"description\":\"The name of the function.\",\"type\":\"string\"},\"parameters\":{\"description\":\"The JSON Schema for the function's parameters.\"},\"type\":{\"const\":\"function\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"function_calling\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"function\\\",\\n \\\"name\\\": \\\"get_weather\\\",\\n \\\"description\\\": \\\"Get the current weather in a given location\\\",\\n \\\"parameters\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\n \\\"location\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"description\\\": \\\"The city and state, e.g. San Francisco, CA\\\"\\n }\\n },\\n \\\"required\\\": [\\\"location\\\"]\\n }\\n }],\\n \\\"input\\\": \\\"What is the weather like in Boston, MA?\\\"\\n }'\\n\"},{\"label\":\"function_calling\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\n \\\"type\\\": \\\"function\\\",\\n \\\"name\\\": \\\"get_weather\\\",\\n \\\"description\\\": \\\"Get the current weather in a given location\\\",\\n \\\"parameters\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\n \\\"location\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"description\\\": \\\"The city and state, e.g. San Francisco, CA\\\"\\n }\\n },\\n \\\"required\\\": [\\\"location\\\"]\\n }\\n }],\\n input=\\\"What is the weather like in Boston?\\\"\\n)\\nprint(response.steps[-1])\\n\"},{\"label\":\"function_calling\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{\\n type: 'function',\\n name: 'get_weather',\\n description: 'Get the current weather in a given location',\\n parameters: {\\n type: 'object',\\n properties: {\\n location: {\\n type: 'string',\\n description: 'The city and state, e.g. San Francisco, CA'\\n }\\n },\\n required: ['location']\\n }\\n }],\\n input: 'What is the weather like in Boston?'\\n});\\nconsole.log(interaction.steps.at(-1));\\n\"},{\"label\":\"function_calling\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Function;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.interactions.Step;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\nimport java.util.Map;\\n\\nClient client = new Client();\\nMap\\u003cString, Object\\u003e parameters = Map.of(\\n \\\"type\\\", \\\"object\\\",\\n \\\"properties\\\", Map.of(\\n \\\"location\\\", Map.of(\\n \\\"type\\\", \\\"string\\\",\\n \\\"description\\\", \\\"The city and state, e.g. San Francisco, CA\\\"\\n )\\n ),\\n \\\"required\\\", List.of(\\\"location\\\")\\n);\\nFunction functionTool = Function.builder()\\n .name(\\\"get_weather\\\")\\n .description(\\\"Get the current weather in a given location\\\")\\n .parameters(parameters)\\n .build();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(functionTool))\\n .input(InteractionsInput.of(\\\"What is the weather like in Boston?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nList\\u003cStep\\u003e steps = interaction.steps().orElse(List.of());\\nif (!steps.isEmpty()) {\\n System.out.println(steps.get(steps.size() - 1));\\n}\\n\"}],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Function\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleSearch\":{\"description\":\"A tool that can be used by the model to search Google.\",\"properties\":{\"search_types\":{\"description\":\"The types of search grounding to enable.\",\"items\":{\"enum\":[\"web_search\",\"image_search\",\"enterprise_web_search\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Setting this field enables web search. Only text results are returned.\",\"Setting this field enables image search. Image bytes are returned.\",\"Setting this field enables enterprise web search.\"]},\"type\":\"array\"},\"type\":{\"const\":\"google_search\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"google_search\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"google_search\\\"\\n }],\\n \\\"input\\\": \\\"Who is the current president of France?\\\"\\n }'\\n\"},{\"label\":\"google_search\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\\"type\\\": \\\"google_search\\\"}],\\n input=\\\"Who is the current president of France?\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"google_search\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{ type: 'google_search' }],\\n input: 'Who is the current president of France?'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"google_search\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.GoogleSearch;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(new GoogleSearch()))\\n .input(InteractionsInput.of(\\\"Who is the current president of France?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"McpServer\":{\"description\":\"A MCPServer is a server that can be called by the model to perform actions.\",\"properties\":{\"allowed_tools\":{\"description\":\"The allowed tools.\",\"items\":{\"$ref\":\"#/$defs/AllowedTools\"},\"type\":\"array\"},\"headers\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"Optional: Fields for authentication headers, timeouts, etc., if needed.\",\"type\":\"object\"},\"name\":{\"description\":\"The name of the MCPServer.\",\"type\":\"string\"},\"type\":{\"const\":\"mcp_server\"},\"url\":{\"description\":\"The full URL for the MCPServer endpoint.\\nExample: \\\"https://api.example.com/mcp\\\"\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"mcp_server\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"mcp_server\\\",\\n \\\"name\\\": \\\"weather_service\\\",\\n \\\"url\\\": \\\"https://gemini-api-demos.uc.r.appspot.com/mcp\\\"\\n }],\\n \\\"input\\\": \\\"Today is 12-05-2025, what is the temperature today in London?\\\"\\n }'\\n\"},{\"label\":\"mcp_server\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\n \\\"type\\\": \\\"mcp_server\\\",\\n \\\"name\\\": \\\"weather_service\\\",\\n \\\"url\\\": \\\"https://gemini-api-demos.uc.r.appspot.com/mcp\\\"\\n }],\\n input=\\\"Today is 12-05-2025, what is the temperature today in London?\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"mcp_server\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{\\n type: 'mcp_server',\\n name: 'weather_service',\\n url: 'https://gemini-api-demos.uc.r.appspot.com/mcp'\\n }],\\n input: 'Today is 12-05-2025, what is the temperature today in London?'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"mcp_server\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.interactions.MCPServer;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nMCPServer mcpTool = MCPServer.builder()\\n .name(\\\"weather_service\\\")\\n .url(\\\"https://gemini-api-demos.uc.r.appspot.com/mcp\\\")\\n .build();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(mcpTool))\\n .input(InteractionsInput.of(\\\"Today is 12-05-2025, what is the temperature today in London?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"MCPServer\"},\"Source\":{\"description\":\"A source to be mounted into the environment.\",\"properties\":{\"content\":{\"description\":\"The inline content if `type` is `INLINE`.\",\"type\":\"string\"},\"encoding\":{\"description\":\"Optional encoding for inline content (e.g. `base64`).\",\"type\":\"string\"},\"source\":{\"description\":\"The source of the environment.\\nFor Cloud Storage, this is the Cloud Storage path.\\nFor GitHub, this is the GitHub path.\",\"type\":\"string\"},\"target\":{\"description\":\"Where the source should appear in the environment.\",\"type\":\"string\"},\"type\":{\"enum\":[\"gcs\",\"inline\",\"repository\",\"skill_registry\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"A Cloud Storage bucket.\",\"Inline content.\",\"A generic repository. The protocol prefix in the source URL\\nidentifies the provider (e.g., github://, gcs://).\",\"A skill resource from the Skill Registry Service.\\nSkill: projects/{project}/locations/{location}/skills/{skill}\\nSkillRevision:\\nprojects/{project}/locations/{location}/skills/{skill}/revisions/{revision}\\nSupport mounting all skills under a project:\\nprojects/{project}/locations/{location}/skills.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"ToolChoiceType\":{\"enum\":[\"auto\",\"any\",\"none\",\"validated\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Auto tool choice.\",\"Any tool choice.\",\"No tool choice.\",\"Validated tool choice.\"],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ToolChoiceType\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"UrlContext\":{\"description\":\"A tool that can be used by the model to fetch URL context.\",\"properties\":{\"type\":{\"const\":\"url_context\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"url_context\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"url_context\\\"\\n }],\\n \\\"input\\\": \\\"Summarize https://www.example.com\\\"\\n }'\\n\"},{\"label\":\"url_context\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\\"type\\\": \\\"url_context\\\"}],\\n input=\\\"Summarize https://www.example.com\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"url_context\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{ type: 'url_context' }],\\n input: 'Summarize https://www.example.com'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"url_context\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.interactions.URLContext;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(new URLContext()))\\n .input(InteractionsInput.of(\\\"Summarize https://www.example.com\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLContext\"\x7d\x7d,\"$ref\":\"#/$defs/Agent\"}", + "CreateCredential": "{\"$defs\":{\"CredentialCreateParams\":{\"description\":\"Represents the fields of a Credential provided on creation.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentVariableConfig\"},{\"$ref\":\"#/$defs/HttpBearerConfig\"},{\"$ref\":\"#/$defs/OAuth2Config\"}],\"required\":[\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"CredentialCreateParams\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"EnvironmentVariableConfig\":{\"description\":\"Configuration for environment variable credentials.\",\"properties\":{\"id\":{\"type\":\"string\"},\"injection_location\":{\"description\":\"Required. Locations where the environment variable can be injected in\\noutgoing HTTP requests. Must contain at least one location.\\nAccepts either a single location (e.g. \\\"header\\\") or an array of locations.\",\"oneOf\":[{\"$ref\":\"#/$defs/InjectionLocation\"},{\"items\":{\"$ref\":\"#/$defs/InjectionLocation\"},\"type\":\"array\"}]},\"trusted_domains\":{\"description\":\"Optional. List of domains allowed to receive this environment variable\\nvalue in HTTP requests.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"type\":{\"const\":\"environment_variable\"},\"value\":{\"description\":\"Required. Input only. Secret value of the environment variable. Write-only; never\\nreturned in responses.\",\"type\":\"string\",\"writeOnly\":true\x7d\x7d,\"required\":[\"id\",\"injection_location\",\"type\",\"value\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"EnvironmentVariableConfig\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"HttpBearerConfig\":{\"description\":\"Configuration for HTTP Bearer token credentials.\",\"properties\":{\"header_name\":{\"description\":\"Optional. Header name to inject the token into. Defaults to\\n'Authorization'.\",\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"prefix\":{\"description\":\"Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\\nfor no prefix.\",\"type\":\"string\"},\"token\":{\"description\":\"Required. Input only. The static bearer token. Write-only; never returned in responses.\",\"type\":\"string\",\"writeOnly\":true},\"type\":{\"const\":\"bearer_token\"\x7d\x7d,\"required\":[\"id\",\"token\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"HttpBearerConfig\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"InjectionLocation\":{\"enum\":[\"header\",\"query\",\"body\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Injected into HTTP request headers.\",\"Injected into HTTP URL query parameters.\",\"Injected into HTTP request body.\"],\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"InjectionLocation\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"OAuth2Config\":{\"description\":\"Configuration for OAuth2 credentials with automatic token refresh.\",\"properties\":{\"client_id\":{\"description\":\"Required. OAuth2 client ID.\",\"type\":\"string\"},\"client_secret\":{\"description\":\"Required. Input only. OAuth2 client secret. Write-only; never returned in responses.\",\"type\":\"string\",\"writeOnly\":true},\"id\":{\"type\":\"string\"},\"refresh_token\":{\"description\":\"Required. Input only. OAuth2 refresh token. Write-only; never returned in responses.\",\"type\":\"string\",\"writeOnly\":true},\"scopes\":{\"description\":\"Optional. List of OAuth2 scopes.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"token_url\":{\"description\":\"Required. OAuth2 token endpoint URL for refreshing access tokens.\",\"type\":\"string\"},\"type\":{\"const\":\"oauth2\"\x7d\x7d,\"required\":[\"client_id\",\"client_secret\",\"id\",\"refresh_token\",\"token_url\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"OAuth2Config\"}],\"x-speakeasy-model-namespace\":\"credentials\"\x7d\x7d,\"$ref\":\"#/$defs/CredentialCreateParams\"}", + "CreateEnvironment": "{\"$defs\":{\"AllowlistEntry\":{\"description\":\"A single domain allowlist rule with optional header injection.\",\"properties\":{\"credential\":{\"description\":\"Optional. Reference to a server-managed Credential resource by ID.\",\"type\":\"string\"},\"domain\":{\"description\":\"Domain to allow outbound requests to. Supports wildcards (e.g. '*.googleapis.com'). Use '*' to allow all domains.\",\"type\":\"string\"},\"transform\":{\"description\":\"Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically.\",\"oneOf\":[{\"description\":\"A list of headers to inject.\",\"items\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"A single header to inject.\",\"type\":\"object\"},\"type\":\"array\"},{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"A single header injection mapping, e.g., {\\\"Authorization\\\": \\\"Bearer token\\\"}.\",\"type\":\"object\"}]\x7d\x7d,\"required\":[\"domain\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"CreateEnvironmentRequest\":{\"description\":\"Request for `CreateEnvironment`.\",\"properties\":{\"from_environment\":{\"description\":\"Optional. The source environment to copy/fork from.\\nFormat: `environments/{environment_id}` or `{environment_id}`.\\nWhen specified, `sources` and `env` must be empty.\",\"type\":\"string\"},\"network\":{\"description\":\"Network configuration for the environment.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentNetworkEgressAllowlist\"},{\"enum\":[\"disabled\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"All network egress is blocked.\"]}]},\"sources\":{\"description\":\"Sources to be mounted into the environment.\",\"items\":{\"$ref\":\"#/$defs/Source\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"environments\",\"name\":\"CreateEnvironmentRequest\"}],\"x-speakeasy-model-namespace\":\"environments\"},\"EnvironmentNetworkEgressAllowlist\":{\"description\":\"Outbound networking configuration for the sandbox. Accepts an object with an 'allowlist' array to restrict traffic, or the string 'disabled' to turn off all network access. Omit entirely to allow all outbound traffic with no header injection.\",\"example\":{\"allowlist\":[{\"domain\":\"github.com\",\"transform\":[{\"Authorization\":\"Bearer your-token\"}]},{\"domain\":\"*.googleapis.com\"}]},\"oneOf\":[{\"description\":\"Outbound networking configuration for the sandbox. When specified, restricts which external domains the sandbox can reach. Omit entirely to allow all outbound traffic with no header injection.\",\"example\":{\"allowlist\":[{\"domain\":\"pypi.org\"},{\"domain\":\"*.github.com\"}]},\"properties\":{\"allowlist\":{\"description\":\"List of allowed outbound domains. Only requests to listed domains are permitted. Use [{'domain': '*'}] to allow all domains while still injecting headers on specific ones.\",\"items\":{\"$ref\":\"#/$defs/AllowlistEntry\"},\"type\":\"array\"\x7d\x7d,\"title\":\"Allowlist\",\"type\":\"object\"},{\"description\":\"Turns all network off.\",\"enum\":[\"disabled\"],\"example\":\"disabled\",\"title\":\"Disabled\",\"type\":\"string\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"Source\":{\"description\":\"A source to be mounted into the environment.\",\"properties\":{\"content\":{\"description\":\"The inline content if `type` is `INLINE`.\",\"type\":\"string\"},\"encoding\":{\"description\":\"Optional encoding for inline content (e.g. `base64`).\",\"type\":\"string\"},\"source\":{\"description\":\"The source of the environment.\\nFor Cloud Storage, this is the Cloud Storage path.\\nFor GitHub, this is the GitHub path.\",\"type\":\"string\"},\"target\":{\"description\":\"Where the source should appear in the environment.\",\"type\":\"string\"},\"type\":{\"enum\":[\"gcs\",\"inline\",\"repository\",\"skill_registry\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"A Cloud Storage bucket.\",\"Inline content.\",\"A generic repository. The protocol prefix in the source URL\\nidentifies the provider (e.g., github://, gcs://).\",\"A skill resource from the Skill Registry Service.\\nSkill: projects/{project}/locations/{location}/skills/{skill}\\nSkillRevision:\\nprojects/{project}/locations/{location}/skills/{skill}/revisions/{revision}\\nSupport mounting all skills under a project:\\nprojects/{project}/locations/{location}/skills.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"\x7d\x7d,\"$ref\":\"#/$defs/CreateEnvironmentRequest\"}", + "CreateInteraction": "{\"$defs\":{\"AgentOption\":{\"description\":\"The agent to interact with.\",\"enum\":[\"deep-research-pro-preview-12-2025\",\"deep-research-preview-04-2026\",\"deep-research-max-preview-04-2026\",\"antigravity-preview-05-2026\"],\"title\":\"Agent\",\"type\":\"string\",\"x-speakeasy-enum-descriptions\":[\"Gemini Deep Research Agent\",\"Gemini Deep Research Agent\",\"Gemini Deep Research Max Agent\",\"Use the Antigravity managed agent to perform multi-step tasks that require reasoning, file operations, and tool use.\"],\"x-speakeasy-enum-format\":\"union\",\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-unknown-values\":\"allow\"},\"AllowedTools\":{\"description\":\"The configuration for allowed tools.\",\"properties\":{\"mode\":{\"$ref\":\"#/$defs/ToolChoiceType\",\"description\":\"The mode of the tool choice.\"},\"tools\":{\"description\":\"The names of the allowed tools.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"AllowedTools\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"AllowlistEntry\":{\"description\":\"A single domain allowlist rule with optional header injection.\",\"properties\":{\"credential\":{\"description\":\"Optional. Reference to a server-managed Credential resource by ID.\",\"type\":\"string\"},\"domain\":{\"description\":\"Domain to allow outbound requests to. Supports wildcards (e.g. '*.googleapis.com'). Use '*' to allow all domains.\",\"type\":\"string\"},\"transform\":{\"description\":\"Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically.\",\"oneOf\":[{\"description\":\"A list of headers to inject.\",\"items\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"A single header to inject.\",\"type\":\"object\"},\"type\":\"array\"},{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"A single header injection mapping, e.g., {\\\"Authorization\\\": \\\"Bearer token\\\"}.\",\"type\":\"object\"}]\x7d\x7d,\"required\":[\"domain\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Annotation\":{\"description\":\"Citation information for model-generated content.\",\"oneOf\":[{\"$ref\":\"#/$defs/FileCitation\"},{\"$ref\":\"#/$defs/PlaceCitation\"},{\"$ref\":\"#/$defs/UrlCitation\"},{\"$ref\":\"#/$defs/WordInfo\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Annotation\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"AntigravityAgentConfig\":{\"description\":\"Configuration for the Antigravity agent runtime.\\nProvides server-side control over the agent's execution environment\\nand tool configuration.\",\"properties\":{\"max_total_tokens\":{\"description\":\"Max total tokens for the agent run.\",\"format\":\"int64\",\"type\":\"string\"},\"model\":{\"description\":\"The model to use for agent reasoning.\",\"type\":\"string\"},\"type\":{\"const\":\"antigravity\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"AntigravityAgentConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"AudioContent\":{\"description\":\"An audio content block.\",\"examples\":[{\"audio\":{\"summary\":\"Audio\",\"value\":{\"data\":\"BASE64_ENCODED_AUDIO\",\"mime_type\":\"audio/wav\",\"type\":\"audio\"\x7d\x7d}],\"properties\":{\"channels\":{\"description\":\"The number of audio channels.\",\"format\":\"int32\",\"type\":\"integer\"},\"data\":{\"description\":\"The audio content.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"mime_type\":{\"description\":\"The mime type of the audio.\",\"enum\":[\"audio/wav\",\"audio/mp3\",\"audio/aiff\",\"audio/aac\",\"audio/ogg\",\"audio/flac\",\"audio/mpeg\",\"audio/m4a\",\"audio/l16\",\"audio/opus\",\"audio/alaw\",\"audio/mulaw\",\"audio/webm\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"WAV audio format\",\"MP3 audio format\",\"AIFF audio format\",\"AAC audio format\",\"OGG audio format\",\"FLAC audio format\",\"MPEG audio format\",\"M4A audio format\",\"L16 audio format\",\"OPUS audio format\",\"ALAW audio format\",\"MULAW audio format\",\"WEBM audio format\"]},\"sample_rate\":{\"description\":\"The sample rate of the audio.\",\"format\":\"int32\",\"type\":\"integer\"},\"type\":{\"const\":\"audio\"},\"uri\":{\"description\":\"The URI of the audio.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"AudioContent\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"AudioResponseFormat\":{\"description\":\"Configuration for audio output format.\",\"examples\":[{\"audio_response_format\":{\"summary\":\"Audio Output\",\"value\":{\"sample_rate\":24000,\"type\":\"audio\"\x7d\x7d}],\"properties\":{\"bit_rate\":{\"description\":\"Bit rate in bits per second (bps). Only applicable for compressed formats\\n(MP3, Opus).\",\"format\":\"int32\",\"type\":\"integer\"},\"delivery\":{\"description\":\"The delivery mode for the audio output.\",\"enum\":[\"inline\",\"uri\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Audio data is returned inline in the response.\",\"Audio data is returned as a URI.\"]},\"mime_type\":{\"description\":\"The MIME type of the audio output.\",\"enum\":[\"audio/mp3\",\"audio/ogg_opus\",\"audio/l16\",\"audio/wav\",\"audio/alaw\",\"audio/mulaw\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"MP3 audio format.\",\"OGG Opus audio format.\",\"Raw PCM (L16) audio format.\",\"WAV audio format.\",\"A-law audio format.\",\"Mu-law audio format.\"]},\"sample_rate\":{\"description\":\"Sample rate in Hz.\",\"format\":\"int32\",\"type\":\"integer\"},\"type\":{\"const\":\"audio\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"AudioResponseFormat\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"CodeExecution\":{\"description\":\"A tool that can be used by the model to execute code.\",\"properties\":{\"type\":{\"const\":\"code_execution\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"code_execution\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"code_execution\\\"\\n }],\\n \\\"input\\\": \\\"Calculate the first 10 Fibonacci numbers\\\"\\n }'\\n\"},{\"label\":\"code_execution\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\\"type\\\": \\\"code_execution\\\"}],\\n input=\\\"Calculate the first 10 Fibonacci numbers\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"code_execution\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{ type: 'code_execution' }],\\n input: 'Calculate the first 10 Fibonacci numbers'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"code_execution\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CodeExecution;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(new CodeExecution()))\\n .input(InteractionsInput.of(\\\"Calculate the first 10 Fibonacci numbers\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"CodeExecutionCallStep\":{\"description\":\"Code execution call step.\",\"examples\":[{\"code_execution_call\":{\"summary\":\"CodeExecutionCallStep\",\"value\":{\"arguments\":{\"code\":\"print(sum(range(1, 11)))\"},\"id\":\"code_call_71021\",\"type\":\"code_execution_call\"\x7d\x7d}],\"properties\":{\"arguments\":{\"$ref\":\"#/$defs/CodeExecutionCallStepArguments\",\"description\":\"Required. The arguments to pass to the code execution.\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"code_execution_call\"\x7d\x7d,\"required\":[\"arguments\",\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"CodeExecutionCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"CodeExecutionCallStepArguments\":{\"description\":\"The arguments to pass to the code execution.\",\"properties\":{\"code\":{\"description\":\"The code to be executed.\",\"type\":\"string\"},\"language\":{\"description\":\"Programming language of the `code`.\",\"enum\":[\"python\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Python \\u003e= 3.10, with numpy and simpy available.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"CodeExecutionCallArguments\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"CodeExecutionCallArguments\"},\"CodeExecutionResultStep\":{\"description\":\"Code execution result step.\",\"examples\":[{\"code_execution_result\":{\"summary\":\"CodeExecutionResultStep\",\"value\":{\"call_id\":\"code_call_71021\",\"result\":\"55\\n\",\"type\":\"code_execution_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"is_error\":{\"description\":\"Whether the code execution resulted in an error.\",\"type\":\"boolean\"},\"result\":{\"description\":\"Required. The output of the code execution.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"code_execution_result\"\x7d\x7d,\"required\":[\"call_id\",\"result\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"CodeExecutionResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"CodeMenderAgentConfig\":{\"description\":\"Configuration for the CodeMender agent.\",\"properties\":{\"find_request\":{\"$ref\":\"#/$defs/FindRequest\",\"description\":\"Parameters for finding vulnerabilities.\"},\"fix_request\":{\"$ref\":\"#/$defs/FixRequest\",\"description\":\"Parameters for fixing vulnerabilities.\"},\"model\":{\"description\":\"The name of the model to use for the CodeMender agent. One\\nCodeMender session will only use one model.\",\"type\":\"string\"},\"session_config\":{\"$ref\":\"#/$defs/SessionConfig\",\"description\":\"Optional session-specific configurations to override default agent\\nbehavior.\"},\"session_id\":{\"description\":\"Parameter for grouping multiple interactions that belong to\\nthe same CodeMender session.\",\"type\":\"string\"},\"type\":{\"const\":\"code-mender\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"CodeMenderAgentConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ComputerUse\":{\"description\":\"A tool that can be used by the model to interact with the computer.\",\"properties\":{\"disabled_safety_policies\":{\"description\":\"Optional. Disabled safety policies for computer use.\",\"items\":{\"enum\":[\"financial_transactions\",\"sensitive_data_modification\",\"communication_tool\",\"account_creation\",\"data_modification\",\"user_consent_management\",\"legal_terms_and_agreements\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Safety policy for financial transactions.\",\"Safety policy for sensitive data modification.\",\"Safety policy for communication tools (e.g. Gmail, Chat, Meet).\",\"Safety policy for account creation.\",\"Safety policy for data modification.\",\"Safety policy for user consent management.\",\"Safety policy for legal terms and agreements.\"]},\"type\":\"array\"},\"enable_prompt_injection_detection\":{\"description\":\"Whether enable the prompt injection detection check on computer-use\\nrequest.\",\"type\":\"boolean\"},\"environment\":{\"description\":\"The environment being operated.\",\"enum\":[\"browser\",\"mobile\",\"desktop\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Operates in a web browser.\",\"Operates in a mobile environment.\",\"Operates in a desktop environment.\"]},\"excluded_predefined_functions\":{\"description\":\"The list of predefined functions that are excluded from the model call.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"type\":{\"const\":\"computer_use\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"computer_use\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-2.5-computer-use-preview-10-2025\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"computer_use\\\"\\n }],\\n \\\"input\\\": \\\"Find a flight to Tokyo\\\"\\n }'\\n\"},{\"label\":\"computer_use\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-2.5-computer-use-preview-10-2025\\\",\\n tools=[{\\\"type\\\": \\\"computer_use\\\"}],\\n input=\\\"Find a flight to Tokyo\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"computer_use\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-2.5-computer-use-preview-10-2025',\\n tools: [{ type: 'computer_use'}],\\n input: 'Find a flight to Tokyo'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"computer_use\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.ComputerUse;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-2.5-computer-use-preview-10-2025\\\")\\n .tools(List.of(new ComputerUse()))\\n .input(InteractionsInput.of(\\\"Find a flight to Tokyo\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"Content\":{\"description\":\"The content of the response.\",\"oneOf\":[{\"$ref\":\"#/$defs/AudioContent\"},{\"$ref\":\"#/$defs/DocumentContent\"},{\"$ref\":\"#/$defs/ImageContent\"},{\"$ref\":\"#/$defs/TextContent\"},{\"$ref\":\"#/$defs/VideoContent\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Content\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"CreateAgentInteractionParams\":{\"description\":\"Parameters for creating agent interactions\",\"properties\":{\"agent\":{\"$ref\":\"#/$defs/AgentOption\",\"description\":\"The name of the `Agent` used for generating the interaction.\"},\"agent_config\":{\"description\":\"Configuration parameters for the agent interaction.\",\"oneOf\":[{\"$ref\":\"#/$defs/AntigravityAgentConfig\"},{\"$ref\":\"#/$defs/CodeMenderAgentConfig\"},{\"$ref\":\"#/$defs/DeepResearchAgentConfig\"},{\"$ref\":\"#/$defs/DynamicAgentConfig\"}]},\"background\":{\"description\":\"Input only. Whether to run the model interaction in the background.\",\"type\":\"boolean\",\"writeOnly\":true},\"created\":{\"description\":\"Required. Output only. The time at which the response was created in ISO 8601 format\\n(YYYY-MM-DDThh:mm:ssZ).\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"environment\":{\"description\":\"The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentConfig\"},{\"type\":\"string\"}]},\"environment_id\":{\"description\":\"Output only. The environment ID for the interaction. Only populated if environment\\nconfig is set in the request.\",\"readOnly\":true,\"type\":\"string\"},\"id\":{\"description\":\"Required. Output only. A unique identifier for the interaction completion.\",\"readOnly\":true,\"type\":\"string\"},\"input\":{\"$ref\":\"#/$defs/InteractionsInput\"},\"labels\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"The labels with user-defined metadata for the request.\",\"type\":\"object\"},\"previous_interaction_id\":{\"description\":\"The ID of the previous interaction, if any.\",\"type\":\"string\"},\"response_format\":{\"description\":\"Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field.\",\"oneOf\":[{\"$ref\":\"#/$defs/ResponseFormat\"},{\"$ref\":\"#/$defs/ResponseFormatList\"}]},\"response_mime_type\":{\"deprecated\":true,\"description\":\"The mime type of the response. This is required if response_format is set.\",\"type\":\"string\"},\"response_modalities\":{\"deprecated\":true,\"description\":\"The requested modalities of the response (TEXT, IMAGE, AUDIO).\",\"items\":{\"$ref\":\"#/$defs/ResponseModality\"},\"type\":\"array\"},\"safety_settings\":{\"description\":\"Safety settings for the interaction.\",\"items\":{\"$ref\":\"#/$defs/SafetySetting\"},\"type\":\"array\"},\"service_tier\":{\"$ref\":\"#/$defs/ServiceTier\",\"description\":\"The service tier for the interaction.\"},\"status\":{\"description\":\"Required. Output only. The status of the interaction.\",\"enum\":[\"in_progress\",\"requires_action\",\"completed\",\"failed\",\"cancelled\",\"incomplete\",\"budget_exceeded\",\"queued\"],\"readOnly\":true,\"type\":\"string\",\"x-google-enum-descriptions\":[\"The interaction is in progress.\",\"The interaction requires action/input from the user.\",\"The interaction is completed.\",\"The interaction failed.\",\"The interaction was cancelled.\",\"The interaction is completed, but contains incomplete results (e.g.\\nhitting max_tokens).\",\"The interaction was halted because the token budget was exceeded.\",\"The interaction is queued, waiting for processing.\"]},\"store\":{\"description\":\"Input only. Whether to store the response and request for later retrieval.\",\"type\":\"boolean\",\"writeOnly\":true},\"stream\":{\"default\":true,\"description\":\"Input only. Whether the interaction is streamed as server-sent events. Defaults to true; set false to receive one complete interaction.\",\"type\":\"boolean\",\"writeOnly\":true},\"system_instruction\":{\"description\":\"System instruction for the interaction.\",\"type\":\"string\"},\"tools\":{\"description\":\"A list of tool declarations the model may call during interaction.\",\"items\":{\"$ref\":\"#/$defs/Tool\"},\"type\":\"array\"},\"updated\":{\"description\":\"Required. Output only. The time at which the response was last updated in ISO 8601 format\\n(YYYY-MM-DDThh:mm:ssZ).\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"webhook_config\":{\"$ref\":\"#/$defs/WebhookConfig\",\"description\":\"Optional. Webhook configuration for receiving notifications when the\\ninteraction completes.\"\x7d\x7d,\"required\":[\"agent\",\"input\"],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"CreateAgentInteractionParamsNonStreaming\",\"representation\":\"input\"},{\"group\":\"interactions\",\"name\":\"CreateAgentInteractionParamsStreaming\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"CreateAgentInteraction\"},\"CreateModelInteractionParams\":{\"description\":\"Parameters for creating model interactions\",\"properties\":{\"background\":{\"description\":\"Input only. Whether to run the model interaction in the background.\",\"type\":\"boolean\",\"writeOnly\":true},\"created\":{\"description\":\"Required. Output only. The time at which the response was created in ISO 8601 format\\n(YYYY-MM-DDThh:mm:ssZ).\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"environment\":{\"description\":\"The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentConfig\"},{\"type\":\"string\"}]},\"environment_id\":{\"description\":\"Output only. The environment ID for the interaction. Only populated if environment\\nconfig is set in the request.\",\"readOnly\":true,\"type\":\"string\"},\"generation_config\":{\"$ref\":\"#/$defs/GenerationConfig\",\"description\":\"Input only. Configuration parameters for the model interaction.\",\"writeOnly\":true},\"id\":{\"description\":\"Required. Output only. A unique identifier for the interaction completion.\",\"readOnly\":true,\"type\":\"string\"},\"input\":{\"$ref\":\"#/$defs/InteractionsInput\"},\"labels\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"The labels with user-defined metadata for the request.\",\"type\":\"object\"},\"model\":{\"$ref\":\"#/$defs/ModelOption\",\"description\":\"The name of the `Model` used for generating the interaction.\"},\"previous_interaction_id\":{\"description\":\"The ID of the previous interaction, if any.\",\"type\":\"string\"},\"response_format\":{\"description\":\"Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field.\",\"oneOf\":[{\"$ref\":\"#/$defs/ResponseFormat\"},{\"$ref\":\"#/$defs/ResponseFormatList\"}]},\"response_mime_type\":{\"deprecated\":true,\"description\":\"The mime type of the response. This is required if response_format is set.\",\"type\":\"string\"},\"response_modalities\":{\"deprecated\":true,\"description\":\"The requested modalities of the response (TEXT, IMAGE, AUDIO).\",\"items\":{\"$ref\":\"#/$defs/ResponseModality\"},\"type\":\"array\"},\"safety_settings\":{\"description\":\"Safety settings for the interaction.\",\"items\":{\"$ref\":\"#/$defs/SafetySetting\"},\"type\":\"array\"},\"service_tier\":{\"$ref\":\"#/$defs/ServiceTier\",\"description\":\"The service tier for the interaction.\"},\"status\":{\"description\":\"Required. Output only. The status of the interaction.\",\"enum\":[\"in_progress\",\"requires_action\",\"completed\",\"failed\",\"cancelled\",\"incomplete\",\"budget_exceeded\",\"queued\"],\"readOnly\":true,\"type\":\"string\",\"x-google-enum-descriptions\":[\"The interaction is in progress.\",\"The interaction requires action/input from the user.\",\"The interaction is completed.\",\"The interaction failed.\",\"The interaction was cancelled.\",\"The interaction is completed, but contains incomplete results (e.g.\\nhitting max_tokens).\",\"The interaction was halted because the token budget was exceeded.\",\"The interaction is queued, waiting for processing.\"]},\"store\":{\"description\":\"Input only. Whether to store the response and request for later retrieval.\",\"type\":\"boolean\",\"writeOnly\":true},\"stream\":{\"default\":true,\"description\":\"Input only. Whether the interaction is streamed as server-sent events. Defaults to true; set false to receive one complete interaction.\",\"type\":\"boolean\",\"writeOnly\":true},\"system_instruction\":{\"description\":\"System instruction for the interaction.\",\"type\":\"string\"},\"tools\":{\"description\":\"A list of tool declarations the model may call during interaction.\",\"items\":{\"$ref\":\"#/$defs/Tool\"},\"type\":\"array\"},\"updated\":{\"description\":\"Required. Output only. The time at which the response was last updated in ISO 8601 format\\n(YYYY-MM-DDThh:mm:ssZ).\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"webhook_config\":{\"$ref\":\"#/$defs/WebhookConfig\",\"description\":\"Optional. Webhook configuration for receiving notifications when the\\ninteraction completes.\"\x7d\x7d,\"required\":[\"input\",\"model\"],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"CreateModelInteractionParamsNonStreaming\",\"representation\":\"input\"},{\"group\":\"interactions\",\"name\":\"CreateModelInteractionParamsStreaming\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"CreateModelInteraction\"},\"DeepResearchAgentConfig\":{\"description\":\"Configuration for the Deep Research agent.\",\"properties\":{\"collaborative_planning\":{\"description\":\"Enables human-in-the-loop planning for the Deep Research agent. If set to\\ntrue, the Deep Research agent will provide a research plan in its response.\\nThe agent will then proceed only if the user confirms the plan in the next\\nturn.\",\"type\":\"boolean\"},\"enable_bigquery_tool\":{\"description\":\"Enables bigquery tool for the Deep Research agent.\",\"type\":\"boolean\"},\"thinking_summaries\":{\"$ref\":\"#/$defs/ThinkingSummaries\",\"description\":\"Whether to include thought summaries in the response.\"},\"type\":{\"const\":\"deep-research\"},\"visualization\":{\"description\":\"Whether to include visualizations in the response.\",\"enum\":[\"off\",\"auto\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Do not include visualizations.\",\"Automatically include visualizations.\"]\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"DeepResearchAgentConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"DocumentContent\":{\"description\":\"A document content block.\",\"examples\":[{\"document\":{\"summary\":\"Document\",\"value\":{\"data\":\"BASE64_ENCODED_DOCUMENT\",\"mime_type\":\"application/pdf\",\"type\":\"document\"\x7d\x7d}],\"properties\":{\"data\":{\"description\":\"The document content.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"mime_type\":{\"description\":\"The mime type of the document.\",\"enum\":[\"application/pdf\",\"text/csv\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"PDF document format\",\"CSV document format\"]},\"type\":{\"const\":\"document\"},\"uri\":{\"description\":\"The URI of the document.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"DocumentContent\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"DynamicAgentConfig\":{\"additionalProperties\":{\"description\":\"For agents that are not supported statically in the API definition.\"},\"description\":\"Configuration for dynamic agents.\",\"properties\":{\"type\":{\"const\":\"dynamic\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"DynamicAgentConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"EnvVar\":{\"description\":\"An environment variable to set in the execution environment.\",\"properties\":{\"credential\":{\"description\":\"Optional reference to a server-managed Credential resource by ID.\",\"type\":\"string\"},\"value\":{\"description\":\"Direct string value for plain environment variables.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"EnvironmentConfig\":{\"description\":\"Configuration for a custom environment.\",\"examples\":[{\"inline_sources\":{\"summary\":\"Inline Sources\",\"value\":{\"sources\":[{\"content\":\"You are a data analyst. Always include visualizations and export results as PDF.\",\"target\":\".agents/AGENTS.md\",\"type\":\"inline\"},{\"content\":\"---\\nname: slide-maker\\ndescription: Create HTML slide decks\\n---\\n# Slide Maker\\n\\nWhen asked to create a presentation:\\n1. Analyze the input data\\n2. Create an HTML slide deck with reveal.js\\n3. Save to /workspace/output/slides.html\",\"target\":\".agents/skills/slide-maker/SKILL.md\",\"type\":\"inline\"}],\"type\":\"remote\"\x7d\x7d},{\"external_sources\":{\"summary\":\"External Sources\",\"value\":{\"sources\":[{\"source\":\"https://github.com/my-org/my-skills.git\",\"target\":\".agents/skills\",\"type\":\"repository\"},{\"source\":\"gs://my-bucket/my-folder\",\"target\":\"/workspace/data\",\"type\":\"gcs\"}],\"type\":\"remote\"\x7d\x7d},{\"network_allowlist\":{\"summary\":\"Network Allowlist\",\"value\":{\"network\":{\"allowlist\":[{\"domain\":\"pypi.org\"},{\"domain\":\"*.github.com\"}]},\"type\":\"remote\"\x7d\x7d},{\"proxy_credentials\":{\"summary\":\"Proxy Credentials\",\"value\":{\"network\":{\"allowlist\":[{\"domain\":\"api.github.com\",\"transform\":{\"Authorization\":\"Bearer YOUR_GITHUB_TOKEN\"\x7d\x7d]},\"type\":\"remote\"\x7d\x7d}],\"properties\":{\"env\":{\"description\":\"Environment variables to set in the sandbox environment.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvVar\"},{\"additionalProperties\":{\"$ref\":\"#/$defs/EnvVar\"},\"type\":\"object\"}]},\"environment_id\":{\"description\":\"Optional. The environment ID for the interaction. If specified, the request will\\nupdate the existing environment instead of creating a new one.\",\"type\":\"string\"},\"network\":{\"description\":\"Network configuration for the environment.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentNetworkEgressAllowlist\"},{\"enum\":[\"disabled\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"All network egress is blocked.\"]}]},\"sources\":{\"items\":{\"$ref\":\"#/$defs/Source\"},\"type\":\"array\"},\"type\":{\"const\":\"remote\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Environment\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"Environment\"},\"EnvironmentNetworkEgressAllowlist\":{\"description\":\"Outbound networking configuration for the sandbox. Accepts an object with an 'allowlist' array to restrict traffic, or the string 'disabled' to turn off all network access. Omit entirely to allow all outbound traffic with no header injection.\",\"example\":{\"allowlist\":[{\"domain\":\"github.com\",\"transform\":[{\"Authorization\":\"Bearer your-token\"}]},{\"domain\":\"*.googleapis.com\"}]},\"oneOf\":[{\"description\":\"Outbound networking configuration for the sandbox. When specified, restricts which external domains the sandbox can reach. Omit entirely to allow all outbound traffic with no header injection.\",\"example\":{\"allowlist\":[{\"domain\":\"pypi.org\"},{\"domain\":\"*.github.com\"}]},\"properties\":{\"allowlist\":{\"description\":\"List of allowed outbound domains. Only requests to listed domains are permitted. Use [{'domain': '*'}] to allow all domains while still injecting headers on specific ones.\",\"items\":{\"$ref\":\"#/$defs/AllowlistEntry\"},\"type\":\"array\"\x7d\x7d,\"title\":\"Allowlist\",\"type\":\"object\"},{\"description\":\"Turns all network off.\",\"enum\":[\"disabled\"],\"example\":\"disabled\",\"title\":\"Disabled\",\"type\":\"string\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ExaAISearchConfig\":{\"description\":\"Used to specify configuration for ExaAISearch.\",\"properties\":{\"api_key\":{\"description\":\"Required. The API key for ExaAiSearch.\",\"type\":\"string\"},\"custom_config\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"Optional. This field can be used to pass any parameter from the Exa.ai Search API.\",\"type\":\"object\"\x7d\x7d,\"required\":[\"api_key\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"FileCitation\":{\"description\":\"A file citation annotation.\",\"properties\":{\"custom_metadata\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"User provided metadata about the retrieved context.\",\"type\":\"object\"},\"document_uri\":{\"description\":\"The URI of the file.\",\"type\":\"string\"},\"end_index\":{\"description\":\"End of the attributed segment, exclusive.\",\"format\":\"int32\",\"type\":\"integer\"},\"file_name\":{\"description\":\"The name of the file.\",\"type\":\"string\"},\"media_id\":{\"description\":\"Media ID in-case of image citations, if applicable.\",\"type\":\"string\"},\"page_number\":{\"description\":\"Page number of the cited document, if applicable.\",\"format\":\"int32\",\"type\":\"integer\"},\"source\":{\"description\":\"Source attributed for a portion of the text.\",\"type\":\"string\"},\"start_index\":{\"description\":\"Start of segment of the response that is attributed to this source.\\n\\nIndex indicates the start of the segment, measured in bytes.\",\"format\":\"int32\",\"type\":\"integer\"},\"type\":{\"const\":\"file_citation\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"FileCitation\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"FileContent\":{\"description\":\"Content of a single file in the codebase.\",\"properties\":{\"content\":{\"description\":\"The UTF-8 encoded text content of the file.\",\"type\":\"string\"},\"path\":{\"description\":\"The relative path of the file from the project root.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"FileSearch\":{\"description\":\"A tool that can be used by the model to search files.\",\"properties\":{\"file_search_store_names\":{\"description\":\"The file search store names to search.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"metadata_filter\":{\"description\":\"Metadata filter to apply to the semantic retrieval documents and chunks.\",\"type\":\"string\"},\"top_k\":{\"description\":\"The number of semantic retrieval chunks to retrieve.\",\"format\":\"int32\",\"type\":\"integer\"},\"type\":{\"const\":\"file_search\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"file_search\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"file_search\\\",\\n \\\"file_search_store_names\\\": [\\\"fileSearchStores/m64d1sevsr4y-xfyawui3fxqg\\\"]\\n }],\\n \\\"input\\\": \\\"Who is the author of the book?\\\"\\n }'\\n\"},{\"label\":\"file_search\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\n\\n# Create a file search store so we have a valid one to use.\\nstore = client.file_search_stores.create()\\n\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\n \\\"type\\\": \\\"file_search\\\",\\n \\\"file_search_store_names\\\": [store.name]\\n }],\\n input=\\\"What documents are available?\\\"\\n)\\nprint(response.output_text)\\n\\n# [cleanup]\\nclient.file_search_stores.delete(name=store.name)\\n# [/cleanup]\\n\"},{\"label\":\"file_search\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\n\\n// Create a file search store so we have a valid one to use.\\nconst store = await ai.fileSearchStores.create({});\\nif (!store.name) {\\n throw new Error('Store creation failed: Name is undefined');\\n}\\n\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{\\n type: 'file_search',\\n file_search_store_names: [store.name]\\n }],\\n input: 'What documents are available?'\\n});\\nconsole.log(interaction.output_text);\\n\\n// [cleanup]\\nawait ai.fileSearchStores.delete({name: store.name});\\n// [/cleanup]\\n\"},{\"label\":\"file_search\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.FileSearch;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport com.google.genai.types.CreateFileSearchStoreConfig;\\nimport com.google.genai.types.FileSearchStore;\\nimport java.util.List;\\n\\nClient client = new Client();\\n\\n// Create a file search store so we have a valid one to use.\\nFileSearchStore store = client.fileSearchStores.create(CreateFileSearchStoreConfig.builder().build());\\nString storeName = store.name().orElseThrow();\\n\\nFileSearch tool = FileSearch.builder()\\n .fileSearchStoreNames(List.of(storeName))\\n .build();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(tool))\\n .input(InteractionsInput.of(\\\"What documents are available?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\\n// [cleanup]\\nclient.fileSearchStores.delete(storeName, null);\\n// [/cleanup]\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"FileSearchCallStep\":{\"description\":\"File Search call step.\",\"examples\":[{\"file_search_call\":{\"summary\":\"FileSearchCallStep\",\"value\":{\"id\":\"file_call_88192\",\"type\":\"file_search_call\"\x7d\x7d}],\"properties\":{\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"file_search_call\"\x7d\x7d,\"required\":[\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"FileSearchCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"FileSearchResultStep\":{\"description\":\"File Search result step.\",\"examples\":[{\"file_search_result\":{\"summary\":\"FileSearchResultStep\",\"value\":{\"call_id\":\"file_call_88192\",\"type\":\"file_search_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"file_search_result\"\x7d\x7d,\"required\":[\"call_id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"FileSearchResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"Filter\":{\"description\":\"Config for filters.\",\"properties\":{\"metadata_filter\":{\"description\":\"Optional. String for metadata filtering.\",\"type\":\"string\"},\"vector_distance_threshold\":{\"description\":\"Optional. Only returns contexts with vector distance smaller than the\\nthreshold.\",\"format\":\"double\",\"type\":\"number\"},\"vector_similarity_threshold\":{\"description\":\"Optional. Only returns contexts with vector similarity larger than the\\nthreshold.\",\"format\":\"double\",\"type\":\"number\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"FindRequest\":{\"description\":\"Request parameters specific to FIND sessions, used for discovering\\nvulnerabilities in a codebase.\",\"properties\":{\"description\":{\"description\":\"Additional context or custom instructions provided by the user to guide\\nthe vulnerability analysis.\",\"type\":\"string\"},\"finding_id\":{\"description\":\"The identifier of a specific finding to verify. This is primarily used in\\nVERIFY mode to focus the agent's execution-based validation on a single\\nvulnerability.\",\"type\":\"string\"},\"mode\":{\"description\":\"The mode of the find session.\",\"enum\":[\"scan\",\"verify\"],\"title\":\"mode\",\"type\":\"string\",\"x-google-enum-descriptions\":[\"Fast scan using only the initial classifier.\",\"Performs classification followed by detailed investigation.\"]},\"source_files\":{\"description\":\"A list of source files to provide as context for the scan.\",\"items\":{\"$ref\":\"#/$defs/FileContent\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"FixRequest\":{\"description\":\"Request parameters specific to FIX sessions, used for generating and\\nvalidating security patches.\",\"properties\":{\"description\":{\"description\":\"Additional context or custom instructions provided by the user to guide\\nthe patch generation process.\",\"type\":\"string\"},\"finding_id\":{\"description\":\"The identifier of the specific security finding to be remediated. This ID\\nmaps to a previously discovered vulnerability.\",\"type\":\"string\"},\"source_files\":{\"description\":\"A list of source files providing context for the remediation. These files\\nare typically the ones containing the identified vulnerability.\",\"items\":{\"$ref\":\"#/$defs/FileContent\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Function\":{\"description\":\"A tool that can be used by the model.\",\"properties\":{\"description\":{\"description\":\"A description of the function.\",\"type\":\"string\"},\"name\":{\"description\":\"The name of the function.\",\"type\":\"string\"},\"parameters\":{\"description\":\"The JSON Schema for the function's parameters.\"},\"type\":{\"const\":\"function\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"function_calling\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"function\\\",\\n \\\"name\\\": \\\"get_weather\\\",\\n \\\"description\\\": \\\"Get the current weather in a given location\\\",\\n \\\"parameters\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\n \\\"location\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"description\\\": \\\"The city and state, e.g. San Francisco, CA\\\"\\n }\\n },\\n \\\"required\\\": [\\\"location\\\"]\\n }\\n }],\\n \\\"input\\\": \\\"What is the weather like in Boston, MA?\\\"\\n }'\\n\"},{\"label\":\"function_calling\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\n \\\"type\\\": \\\"function\\\",\\n \\\"name\\\": \\\"get_weather\\\",\\n \\\"description\\\": \\\"Get the current weather in a given location\\\",\\n \\\"parameters\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\n \\\"location\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"description\\\": \\\"The city and state, e.g. San Francisco, CA\\\"\\n }\\n },\\n \\\"required\\\": [\\\"location\\\"]\\n }\\n }],\\n input=\\\"What is the weather like in Boston?\\\"\\n)\\nprint(response.steps[-1])\\n\"},{\"label\":\"function_calling\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{\\n type: 'function',\\n name: 'get_weather',\\n description: 'Get the current weather in a given location',\\n parameters: {\\n type: 'object',\\n properties: {\\n location: {\\n type: 'string',\\n description: 'The city and state, e.g. San Francisco, CA'\\n }\\n },\\n required: ['location']\\n }\\n }],\\n input: 'What is the weather like in Boston?'\\n});\\nconsole.log(interaction.steps.at(-1));\\n\"},{\"label\":\"function_calling\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Function;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.interactions.Step;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\nimport java.util.Map;\\n\\nClient client = new Client();\\nMap\\u003cString, Object\\u003e parameters = Map.of(\\n \\\"type\\\", \\\"object\\\",\\n \\\"properties\\\", Map.of(\\n \\\"location\\\", Map.of(\\n \\\"type\\\", \\\"string\\\",\\n \\\"description\\\", \\\"The city and state, e.g. San Francisco, CA\\\"\\n )\\n ),\\n \\\"required\\\", List.of(\\\"location\\\")\\n);\\nFunction functionTool = Function.builder()\\n .name(\\\"get_weather\\\")\\n .description(\\\"Get the current weather in a given location\\\")\\n .parameters(parameters)\\n .build();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(functionTool))\\n .input(InteractionsInput.of(\\\"What is the weather like in Boston?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nList\\u003cStep\\u003e steps = interaction.steps().orElse(List.of());\\nif (!steps.isEmpty()) {\\n System.out.println(steps.get(steps.size() - 1));\\n}\\n\"}],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Function\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"FunctionCallStep\":{\"description\":\"A function tool call step.\",\"examples\":[{\"function_call\":{\"summary\":\"FunctionCallStep\",\"value\":{\"arguments\":{\"location\":\"Boston, MA\"},\"id\":\"call_98231\",\"name\":\"get_weather\",\"type\":\"function_call\"\x7d\x7d}],\"properties\":{\"arguments\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"Required. The arguments to pass to the function.\",\"type\":\"object\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"name\":{\"description\":\"Required. The name of the tool to call.\",\"type\":\"string\"},\"type\":{\"const\":\"function_call\"\x7d\x7d,\"required\":[\"arguments\",\"id\",\"name\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"FunctionCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"FunctionResultStep\":{\"description\":\"Result of a function tool call.\",\"examples\":[{\"function_result\":{\"summary\":\"FunctionResultStep\",\"value\":{\"call_id\":\"call_98231\",\"name\":\"get_weather\",\"result\":[{\"text\":\"{\\\"weather\\\":\\\"sunny\\\"}\",\"type\":\"text\"}],\"type\":\"function_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"is_error\":{\"description\":\"Whether the tool call resulted in an error.\",\"type\":\"boolean\"},\"name\":{\"description\":\"The name of the tool that was called.\",\"type\":\"string\"},\"result\":{\"description\":\"Required. The result of the tool call.\",\"oneOf\":[{\"items\":{\"$ref\":\"#/$defs/FunctionResultSubcontent\"},\"title\":\"FunctionResultSubcontentList\",\"type\":\"array\"},{\"type\":\"object\"},{\"type\":\"string\"}]},\"type\":{\"const\":\"function_result\"\x7d\x7d,\"required\":[\"call_id\",\"result\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"FunctionResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"FunctionResultSubcontent\":{\"oneOf\":[{\"$ref\":\"#/$defs/ImageContent\"},{\"$ref\":\"#/$defs/TextContent\"}],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"GenerationConfig\":{\"description\":\"Configuration parameters for model interactions.\",\"properties\":{\"image_config\":{\"$ref\":\"#/$defs/ImageConfig\",\"deprecated\":true,\"description\":\"Configuration for image interaction.\"},\"max_output_tokens\":{\"description\":\"The maximum number of tokens to include in the response.\",\"format\":\"int32\",\"type\":\"integer\"},\"seed\":{\"description\":\"Seed used in decoding for reproducibility.\",\"format\":\"int32\",\"type\":\"integer\"},\"speech_config\":{\"description\":\"Optional. Speech and multi-speaker configuration.\",\"oneOf\":[{\"$ref\":\"#/$defs/SpeakerConfig\"},{\"items\":{\"$ref\":\"#/$defs/SpeechConfig\"},\"type\":\"array\"}]},\"stop_sequences\":{\"description\":\"A list of character sequences that will stop output interaction.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"thinking_level\":{\"$ref\":\"#/$defs/ThinkingLevel\",\"description\":\"The level of thought tokens that the model should generate.\"},\"thinking_summaries\":{\"$ref\":\"#/$defs/ThinkingSummaries\",\"description\":\"Whether to include thought summaries in the response.\"},\"tool_choice\":{\"description\":\"The tool choice configuration.\",\"oneOf\":[{\"$ref\":\"#/$defs/ToolChoiceConfig\"},{\"$ref\":\"#/$defs/ToolChoiceType\"}]},\"transcription_config\":{\"$ref\":\"#/$defs/TranscriptionConfig\",\"description\":\"Optional. Configuration for speech recognition (transcription). If present, ASR is\\nenabled.\"},\"video_config\":{\"$ref\":\"#/$defs/VideoConfig\",\"description\":\"Configuration for video generation.\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GenerationConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleMaps\":{\"description\":\"A tool that can be used by the model to call Google Maps.\",\"properties\":{\"enable_widget\":{\"description\":\"Whether to return a widget context token in the tool call result of the\\nresponse.\",\"type\":\"boolean\"},\"latitude\":{\"description\":\"The latitude of the user's location.\",\"format\":\"double\",\"type\":\"number\"},\"longitude\":{\"description\":\"The longitude of the user's location.\",\"format\":\"double\",\"type\":\"number\"},\"type\":{\"const\":\"google_maps\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"google_maps\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"google_maps\\\",\\n \\\"latitude\\\": 37.7749,\\n \\\"longitude\\\": -122.4194\\n }],\\n \\\"input\\\": \\\"What is the best food near me?\\\"\\n }'\\n\"},{\"label\":\"google_maps\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\n \\\"type\\\": \\\"google_maps\\\",\\n \\\"latitude\\\": 37.7749,\\n \\\"longitude\\\": -122.4194\\n }],\\n input=\\\"What is the best food near me?\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"google_maps\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{\\n type: 'google_maps',\\n latitude: 37.7749,\\n longitude: -122.4194\\n }],\\n input: 'What is the best food near me?'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"google_maps\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.GoogleMaps;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nGoogleMaps tool = GoogleMaps.builder()\\n .latitude(37.7749)\\n .longitude(-122.4194)\\n .build();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(tool))\\n .input(InteractionsInput.of(\\\"What is the best food near me?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleMapsCallStep\":{\"description\":\"Google Maps call step.\",\"examples\":[{\"google_maps_call\":{\"summary\":\"GoogleMapsCallStep\",\"value\":{\"arguments\":{\"latitude\":37.7749,\"longitude\":-122.4194},\"id\":\"maps_call_39201\",\"type\":\"google_maps_call\"\x7d\x7d}],\"properties\":{\"arguments\":{\"$ref\":\"#/$defs/GoogleMapsCallStepArguments\",\"description\":\"The arguments to pass to the Google Maps tool.\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"google_maps_call\"\x7d\x7d,\"required\":[\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleMapsCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleMapsCallStepArguments\":{\"description\":\"The arguments to pass to the Google Maps tool.\",\"properties\":{\"queries\":{\"description\":\"The queries to be executed.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleMapsCallArguments\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"GoogleMapsCallArguments\"},\"GoogleMapsResultItem\":{\"description\":\"The result of the Google Maps.\",\"properties\":{\"places\":{\"items\":{\"$ref\":\"#/$defs/GoogleMapsResultPlaces\"},\"type\":\"array\"},\"widget_context_token\":{\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleMapsResult\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"GoogleMapsResult\"},\"GoogleMapsResultPlaces\":{\"properties\":{\"name\":{\"type\":\"string\"},\"place_id\":{\"type\":\"string\"},\"review_snippets\":{\"items\":{\"$ref\":\"#/$defs/ReviewSnippet\"},\"type\":\"array\"},\"url\":{\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleMapsResultStep\":{\"description\":\"Google Maps result step.\",\"examples\":[{\"google_maps_result\":{\"summary\":\"GoogleMapsResultStep\",\"value\":{\"call_id\":\"maps_call_39201\",\"result\":[{\"name\":\"Golden Gate Park\",\"place_id\":\"ChIJIQBpAG2ahYAR9R7bNdTLg8M\",\"rating\":4.8}],\"type\":\"google_maps_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"result\":{\"items\":{\"$ref\":\"#/$defs/GoogleMapsResultItem\"},\"type\":\"array\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"google_maps_result\"\x7d\x7d,\"required\":[\"call_id\",\"result\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleMapsResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleSearch\":{\"description\":\"A tool that can be used by the model to search Google.\",\"properties\":{\"search_types\":{\"description\":\"The types of search grounding to enable.\",\"items\":{\"enum\":[\"web_search\",\"image_search\",\"enterprise_web_search\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Setting this field enables web search. Only text results are returned.\",\"Setting this field enables image search. Image bytes are returned.\",\"Setting this field enables enterprise web search.\"]},\"type\":\"array\"},\"type\":{\"const\":\"google_search\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"google_search\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"google_search\\\"\\n }],\\n \\\"input\\\": \\\"Who is the current president of France?\\\"\\n }'\\n\"},{\"label\":\"google_search\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\\"type\\\": \\\"google_search\\\"}],\\n input=\\\"Who is the current president of France?\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"google_search\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{ type: 'google_search' }],\\n input: 'Who is the current president of France?'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"google_search\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.GoogleSearch;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(new GoogleSearch()))\\n .input(InteractionsInput.of(\\\"Who is the current president of France?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleSearchCallStep\":{\"description\":\"Google Search call step.\",\"examples\":[{\"google_search_call\":{\"summary\":\"GoogleSearchCallStep\",\"value\":{\"arguments\":{\"query\":\"Who won the men's 100m in Paris 2024?\"},\"id\":\"search_call_19201\",\"type\":\"google_search_call\"\x7d\x7d}],\"properties\":{\"arguments\":{\"$ref\":\"#/$defs/GoogleSearchCallStepArguments\",\"description\":\"Required. The arguments to pass to Google Search.\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"search_type\":{\"description\":\"The type of search grounding enabled.\",\"enum\":[\"web_search\",\"image_search\",\"enterprise_web_search\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Setting this field enables web search. Only text results are returned.\",\"Setting this field enables image search. Image bytes are returned.\",\"Setting this field enables enterprise web search.\"]},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"google_search_call\"\x7d\x7d,\"required\":[\"arguments\",\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleSearchCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"GoogleSearchCallStepArguments\":{\"description\":\"The arguments to pass to Google Search.\",\"properties\":{\"queries\":{\"description\":\"Web search queries for the following-up web search.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleSearchCallArguments\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"GoogleSearchCallArguments\"},\"GoogleSearchResultItem\":{\"description\":\"The result of the Google Search.\",\"properties\":{\"search_suggestions\":{\"description\":\"Web content snippet that can be embedded in a web page or an app webview.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleSearchResult\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"GoogleSearchResult\"},\"GoogleSearchResultStep\":{\"description\":\"Google Search result step.\",\"examples\":[{\"google_search_result\":{\"summary\":\"GoogleSearchResultStep\",\"value\":{\"call_id\":\"search_call_19201\",\"result\":[{\"snippet\":\"American Noah Lyles won the Olympic men's 100m gold medal in a photo finish.\",\"title\":\"Paris 2024 Olympics: Noah Lyles wins men's 100m gold\",\"url\":\"https://olympics.com/en/news/paris-2024-noah-lyles-wins-mens-100m-gold\"}],\"type\":\"google_search_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"is_error\":{\"description\":\"Whether the Google Search resulted in an error.\",\"type\":\"boolean\"},\"result\":{\"description\":\"Required. The results of the Google Search.\",\"items\":{\"$ref\":\"#/$defs/GoogleSearchResultItem\"},\"type\":\"array\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"google_search_result\"\x7d\x7d,\"required\":[\"call_id\",\"result\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"GoogleSearchResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"HarmCategory\":{\"enum\":[\"hate_speech\",\"dangerous_content\",\"harassment\",\"sexually_explicit\",\"civic_integrity\",\"image_hate\",\"image_dangerous_content\",\"image_harassment\",\"image_sexually_explicit\",\"jailbreak\"],\"type\":\"string\",\"x-google-enum-deprecated\":[false,false,false,false,true,false,false,false,false,false],\"x-google-enum-descriptions\":[\"Content that promotes violence or incites hatred against individuals or\\ngroups based on certain attributes.\",\"Content that promotes, facilitates, or enables dangerous activities.\",\"Abusive, threatening, or content intended to bully, torment, or ridicule.\",\"Content that contains sexually explicit material.\",\"Deprecated: Election filter is not longer supported.\\nThe harm category is civic integrity.\",\"Images that contain hate speech.\",\"Images that contain dangerous content.\",\"Images that contain harassment.\",\"Images that contain sexually explicit content.\",\"Prompts designed to bypass safety filters.\"],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"HarmCategory\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"HybridSearch\":{\"description\":\"Config for Hybrid Search.\",\"properties\":{\"alpha\":{\"description\":\"Optional. Alpha value controls the weight between dense and sparse vector search\\nresults.\",\"format\":\"float\",\"type\":\"number\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"ImageConfig\":{\"deprecated\":true,\"description\":\"The configuration for image interaction.\",\"properties\":{\"aspect_ratio\":{\"enum\":[\"1:1\",\"2:3\",\"3:2\",\"3:4\",\"4:3\",\"4:5\",\"5:4\",\"9:16\",\"16:9\",\"21:9\",\"1:8\",\"8:1\",\"1:4\",\"4:1\"],\"x-google-enum-descriptions\":[\"1:1 aspect ratio.\",\"2:3 aspect ratio.\",\"3:2 aspect ratio.\",\"3:4 aspect ratio.\",\"4:3 aspect ratio.\",\"4:5 aspect ratio.\",\"5:4 aspect ratio.\",\"9:16 aspect ratio.\",\"16:9 aspect ratio.\",\"21:9 aspect ratio.\",\"1:8 aspect ratio.\",\"8:1 aspect ratio.\",\"1:4 aspect ratio.\",\"4:1 aspect ratio.\"]},\"image_size\":{\"enum\":[\"1K\",\"2K\",\"4K\",\"512\"],\"x-google-enum-descriptions\":[\"1K image size.\",\"2K image size.\",\"4K image size.\",\"512 image size.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ImageConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ImageContent\":{\"description\":\"An image content block.\",\"examples\":[{\"image\":{\"summary\":\"Image\",\"value\":{\"data\":\"BASE64_ENCODED_IMAGE\",\"mime_type\":\"image/png\",\"type\":\"image\"\x7d\x7d}],\"properties\":{\"data\":{\"description\":\"The image content.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"mime_type\":{\"description\":\"The mime type of the image.\",\"enum\":[\"image/png\",\"image/jpeg\",\"image/webp\",\"image/heic\",\"image/heif\",\"image/gif\",\"image/bmp\",\"image/tiff\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"PNG image format\",\"JPEG image format\",\"WebP image format\",\"HEIC image format\",\"HEIF image format\",\"GIF image format\",\"BMP image format\",\"TIFF image format\"]},\"resolution\":{\"$ref\":\"#/$defs/MediaResolution\",\"description\":\"The resolution of the media.\"},\"type\":{\"const\":\"image\"},\"uri\":{\"description\":\"The URI of the image.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ImageContent\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ImageResponseFormat\":{\"description\":\"Configuration for image output format.\",\"examples\":[{\"image_response_format\":{\"summary\":\"Image Output\",\"value\":{\"aspect_ratio\":\"16:9\",\"image_size\":\"1K\",\"mime_type\":\"image/jpeg\",\"type\":\"image\"\x7d\x7d}],\"properties\":{\"aspect_ratio\":{\"description\":\"The aspect ratio for the image output.\",\"enum\":[\"1:1\",\"2:3\",\"3:2\",\"3:4\",\"4:3\",\"4:5\",\"5:4\",\"9:16\",\"16:9\",\"21:9\",\"1:8\",\"8:1\",\"1:4\",\"4:1\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"1:1 aspect ratio.\",\"2:3 aspect ratio.\",\"3:2 aspect ratio.\",\"3:4 aspect ratio.\",\"4:3 aspect ratio.\",\"4:5 aspect ratio.\",\"5:4 aspect ratio.\",\"9:16 aspect ratio.\",\"16:9 aspect ratio.\",\"21:9 aspect ratio.\",\"1:8 aspect ratio.\",\"8:1 aspect ratio.\",\"1:4 aspect ratio.\",\"4:1 aspect ratio.\"]},\"delivery\":{\"description\":\"The delivery mode for the image output.\",\"enum\":[\"inline\",\"uri\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Image data is returned inline in the response.\",\"Image data is returned as a URI.\"]},\"image_size\":{\"description\":\"The size of the image output.\",\"enum\":[\"512\",\"1K\",\"2K\",\"4K\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"512px image size.\",\"1K image size.\",\"2K image size.\",\"4K image size.\"]},\"mime_type\":{\"description\":\"The MIME type of the image output.\",\"enum\":[\"image/jpeg\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"JPEG image format.\"]},\"type\":{\"const\":\"image\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ImageResponseFormat\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"InteractionsInput\":{\"description\":\"The input for the interaction.\",\"oneOf\":[{\"$ref\":\"#/$defs/Content\"},{\"items\":{\"$ref\":\"#/$defs/Step\"},\"title\":\"StepList\",\"type\":\"array\"},{\"items\":{\"$ref\":\"#/$defs/Content\"},\"title\":\"ContentList\",\"type\":\"array\"},{\"type\":\"string\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"McpServer\":{\"description\":\"A MCPServer is a server that can be called by the model to perform actions.\",\"properties\":{\"allowed_tools\":{\"description\":\"The allowed tools.\",\"items\":{\"$ref\":\"#/$defs/AllowedTools\"},\"type\":\"array\"},\"headers\":{\"additionalProperties\":{\"type\":\"string\"},\"description\":\"Optional: Fields for authentication headers, timeouts, etc., if needed.\",\"type\":\"object\"},\"name\":{\"description\":\"The name of the MCPServer.\",\"type\":\"string\"},\"type\":{\"const\":\"mcp_server\"},\"url\":{\"description\":\"The full URL for the MCPServer endpoint.\\nExample: \\\"https://api.example.com/mcp\\\"\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"mcp_server\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"mcp_server\\\",\\n \\\"name\\\": \\\"weather_service\\\",\\n \\\"url\\\": \\\"https://gemini-api-demos.uc.r.appspot.com/mcp\\\"\\n }],\\n \\\"input\\\": \\\"Today is 12-05-2025, what is the temperature today in London?\\\"\\n }'\\n\"},{\"label\":\"mcp_server\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\n \\\"type\\\": \\\"mcp_server\\\",\\n \\\"name\\\": \\\"weather_service\\\",\\n \\\"url\\\": \\\"https://gemini-api-demos.uc.r.appspot.com/mcp\\\"\\n }],\\n input=\\\"Today is 12-05-2025, what is the temperature today in London?\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"mcp_server\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{\\n type: 'mcp_server',\\n name: 'weather_service',\\n url: 'https://gemini-api-demos.uc.r.appspot.com/mcp'\\n }],\\n input: 'Today is 12-05-2025, what is the temperature today in London?'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"mcp_server\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.interactions.MCPServer;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nMCPServer mcpTool = MCPServer.builder()\\n .name(\\\"weather_service\\\")\\n .url(\\\"https://gemini-api-demos.uc.r.appspot.com/mcp\\\")\\n .build();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(mcpTool))\\n .input(InteractionsInput.of(\\\"Today is 12-05-2025, what is the temperature today in London?\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"MCPServer\"},\"McpServerToolCallStep\":{\"description\":\"MCPServer tool call step.\",\"examples\":[{\"mcp_server_tool_call\":{\"summary\":\"McpServerToolCallStep\",\"value\":{\"arguments\":{\"income\":120000,\"state\":\"CA\"},\"id\":\"mcp_call_29012\",\"name\":\"calculate_tax\",\"server_name\":\"financial_mcp_server\",\"type\":\"mcp_server_tool_call\"\x7d\x7d}],\"properties\":{\"arguments\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"Required. The JSON object of arguments for the function.\",\"type\":\"object\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"name\":{\"description\":\"Required. The name of the tool which was called.\",\"type\":\"string\"},\"server_name\":{\"description\":\"Required. The name of the used MCP server.\",\"type\":\"string\"},\"type\":{\"const\":\"mcp_server_tool_call\"\x7d\x7d,\"required\":[\"arguments\",\"id\",\"name\",\"server_name\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"MCPServerToolCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"MCPServerToolCallStep\"},\"McpServerToolResultStep\":{\"description\":\"MCPServer tool result step.\",\"examples\":[{\"mcp_server_tool_result\":{\"summary\":\"McpServerToolResultStep\",\"value\":{\"call_id\":\"mcp_call_29012\",\"result\":{\"tax_due\":32400},\"type\":\"mcp_server_tool_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"name\":{\"description\":\"Name of the tool which is called for this specific tool call.\",\"type\":\"string\"},\"result\":{\"description\":\"Required. The output from the MCP server call. Can be simple text or rich content.\",\"oneOf\":[{\"items\":{\"$ref\":\"#/$defs/FunctionResultSubcontent\"},\"title\":\"FunctionResultSubcontentList\",\"type\":\"array\"},{\"type\":\"object\"},{\"type\":\"string\"}]},\"server_name\":{\"description\":\"The name of the used MCP server.\",\"type\":\"string\"},\"type\":{\"const\":\"mcp_server_tool_result\"\x7d\x7d,\"required\":[\"call_id\",\"result\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"MCPServerToolResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"MCPServerToolResultStep\"},\"MediaProcessing\":{\"oneOf\":[{\"$ref\":\"#/$defs/StaticMediaProcessing\"}],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"MediaResolution\":{\"enum\":[\"low\",\"medium\",\"high\",\"ultra_high\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Low resolution.\",\"Medium resolution.\",\"High resolution.\",\"Ultra high resolution.\"],\"x-speakeasy-model-namespace\":\"interactions\"},\"ModelOption\":{\"default\":\"gemini-3.6-flash\",\"description\":\"The model that will complete your prompt.\\\\n\\\\nSee [models](https://ai.google.dev/gemini-api/docs/models) for additional details.\",\"enum\":[\"gemini-2.5-flash\",\"gemini-2.5-pro\",\"gemma-4-26b-a4b-it\",\"gemma-4-31b-it\",\"gemini-flash-latest\",\"gemini-flash-lite-latest\",\"gemini-pro-latest\",\"gemini-2.5-flash-lite\",\"gemini-2.5-flash-image\",\"gemini-3-flash-preview\",\"gemini-3.1-pro-preview\",\"gemini-3.1-pro-preview-customtools\",\"gemini-3.1-flash-lite\",\"gemini-3-pro-image\",\"nano-banana-pro-preview\",\"gemini-3.1-flash-image\",\"gemini-3.5-flash\",\"gemini-3.6-flash\",\"gemini-3.7-flash\",\"gemini-3.8-flash\",\"lyria-3-clip-preview\",\"lyria-3-pro-preview\",\"gemini-robotics-er-1.6-preview\",\"gemini-robotics-er-2-preview\"],\"title\":\"Model\",\"type\":\"string\",\"x-speakeasy-cli-catalog\":{\"command\":\"models\",\"default\":\"gemini-3.6-flash\",\"description\":\"Curated model list generated from the API schema. The default model is used by \\\"agent run\\\" whenever the request body names neither \\\"model\\\" nor \\\"agent\\\".\",\"summary\":\"List available models and the default\"},\"x-speakeasy-enum-descriptions\":[\"Our first hybrid reasoning model which supports a 1M token context window and has thinking budgets.\",\"Our state-of-the-art multipurpose model, which excels at coding and complex reasoning tasks.\",\"Gemma 4 26B A4B IT\",\"Gemma 4 31B IT\",\"Latest release of Gemini Flash\",\"Latest release of Gemini Flash-Lite\",\"Latest release of Gemini Pro\",\"Our smallest and most cost effective model, built for at scale usage.\",\"Our native image generation model, optimized for speed, flexibility, and contextual understanding. Text input and output is priced the same as 2.5 Flash.\",\"Our most intelligent model built for speed, combining frontier intelligence with superior search and grounding.\",\"Our latest SOTA reasoning model with unprecedented depth and nuance, and powerful multimodal understanding and coding capabilities.\",\"Gemini 3.1 Pro Preview optimized for custom tool usage\",\"Our most cost-efficient model, optimized for high-volume agentic tasks, translation, and simple data processing.\",\"Gemini 3 Pro Image\",\"Gemini 3 Pro Image Preview\",\"Gemini 3.1 Flash Image.\",\"Gemini 3.5 Flash - Our earlier Flash model, built for speed and foundational performance across routine, high-throughput workloads.\",\"Gemini 3.6 Flash - Our previous generation Flash model, balancing speed and multimodal capabilities across general agentic and everyday tasks.\",\"Gemini 3.7 Flash - Our high-speed, efficient Flash model built for everyday coding, agentic tool use, and reliable multi-step execution.\",\"Gemini 3.8 Flash - Our most intelligent Flash model, engineered for long-horizon software engineering, autonomous agents, and complex enterprise workflows.\",\"Our low-latency, music generation model optimized for high-fidelity audio clips and precise rhythmic control.\",\"Our advanced, full-song generative model with deep compositional understanding, optimized for precise structural control and complex transitions across diverse musical styles.\",\"Gemini Robotics-ER 1.6 Preview\",\"Gemini Robotics Embodied Reasoning 2 Preview\"],\"x-speakeasy-enum-format\":\"union\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Model\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"Model\",\"x-speakeasy-unknown-values\":\"allow\"},\"ModelOutputStep\":{\"description\":\"Output generated by the model.\",\"examples\":[{\"model_output\":{\"summary\":\"ModelOutputStep\",\"value\":{\"content\":[{\"text\":\"The capital of France is Paris.\",\"type\":\"text\"}],\"type\":\"model_output\"\x7d\x7d}],\"properties\":{\"content\":{\"items\":{\"$ref\":\"#/$defs/Content\"},\"type\":\"array\"},\"error\":{\"$ref\":\"#/$defs/Status\",\"deprecated\":true,\"description\":\"The error result of the operation in case of failure or cancellation.\"},\"type\":{\"const\":\"model_output\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ModelOutputStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ParallelAISearchConfig\":{\"description\":\"Used to specify configuration for ParallelAISearch.\",\"properties\":{\"api_key\":{\"description\":\"Optional. The API key for ParallelAiSearch.\",\"type\":\"string\"},\"custom_config\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"Optional. Custom configs for ParallelAiSearch.\",\"type\":\"object\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"PlaceCitation\":{\"description\":\"A place citation annotation.\",\"properties\":{\"end_index\":{\"description\":\"End of the attributed segment, exclusive.\",\"format\":\"int32\",\"type\":\"integer\"},\"name\":{\"description\":\"Title of the place.\",\"type\":\"string\"},\"place_id\":{\"description\":\"The ID of the place, in `places/{place_id}` format.\",\"type\":\"string\"},\"review_snippets\":{\"description\":\"Snippets of reviews that are used to generate answers about the\\nfeatures of a given place in Google Maps.\",\"items\":{\"$ref\":\"#/$defs/ReviewSnippet\"},\"type\":\"array\"},\"start_index\":{\"description\":\"Start of segment of the response that is attributed to this source.\\n\\nIndex indicates the start of the segment, measured in bytes.\",\"format\":\"int32\",\"type\":\"integer\"},\"type\":{\"const\":\"place_citation\"},\"url\":{\"description\":\"URI reference of the place.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"PlaceCitation\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ProcessingCallStep\":{\"description\":\"A server-initiated processing step for media analysis (e.g. video\\nunderstanding).\",\"properties\":{\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"processing_call\"\x7d\x7d,\"required\":[\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ProcessingCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ProcessingResultStep\":{\"description\":\"The result of a server-initiated media processing step.\",\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"processing_result\"\x7d\x7d,\"required\":[\"call_id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ProcessingResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"RagResource\":{\"description\":\"The definition of the Rag resource.\",\"properties\":{\"rag_corpus\":{\"description\":\"Optional. RagCorpora resource name.\",\"type\":\"string\"},\"rag_file_ids\":{\"description\":\"Optional. rag_file_id. The files should be in the same rag_corpus set in\\nrag_corpus field.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"RagRetrievalConfig\":{\"description\":\"Specifies the context retrieval config.\",\"properties\":{\"filter\":{\"$ref\":\"#/$defs/Filter\",\"description\":\"Optional. Config for filters.\"},\"hybrid_search\":{\"$ref\":\"#/$defs/HybridSearch\",\"description\":\"Optional. Config for Hybrid Search.\"},\"ranking\":{\"$ref\":\"#/$defs/Ranking\",\"description\":\"Optional. Config for ranking and reranking.\"},\"top_k\":{\"description\":\"Optional. The number of contexts to retrieve.\",\"format\":\"int32\",\"type\":\"integer\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"RagStoreConfig\":{\"description\":\"Use to specify configuration for RAG Store.\",\"properties\":{\"rag_resources\":{\"description\":\"Optional. The representation of the rag source.\",\"items\":{\"$ref\":\"#/$defs/RagResource\"},\"type\":\"array\"},\"rag_retrieval_config\":{\"$ref\":\"#/$defs/RagRetrievalConfig\",\"description\":\"Optional. The retrieval config for the Rag query.\"},\"similarity_top_k\":{\"deprecated\":true,\"description\":\"Optional. Number of top k results to return from the selected corpora.\",\"format\":\"int32\",\"type\":\"integer\"},\"vector_distance_threshold\":{\"deprecated\":true,\"description\":\"Optional. Only return results with vector distance smaller than the threshold.\",\"format\":\"double\",\"type\":\"number\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"RankService\":{\"description\":\"Config for Rank Service.\",\"properties\":{\"model_name\":{\"description\":\"Optional. The model name of the rank service.\",\"type\":\"string\"},\"ranking_config\":{\"const\":\"rank_service\"\x7d\x7d,\"required\":[\"ranking_config\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Ranking\":{\"$ref\":\"#/$defs/RankService\",\"description\":\"Config for ranking and reranking.\",\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"ResponseFormat\":{\"oneOf\":[{\"$ref\":\"#/$defs/AudioResponseFormat\"},{\"$ref\":\"#/$defs/ImageResponseFormat\"},{\"$ref\":\"#/$defs/TextResponseFormat\"},{\"$ref\":\"#/$defs/VideoResponseFormat\"},{\"additionalProperties\":true,\"type\":\"object\"}],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"ResponseFormatList\":{\"example\":[{\"mime_type\":\"application/json\",\"type\":\"text\"}],\"items\":{\"$ref\":\"#/$defs/ResponseFormat\"},\"type\":\"array\",\"x-speakeasy-model-namespace\":\"interactions\"},\"ResponseModality\":{\"enum\":[\"text\",\"image\",\"audio\",\"video\",\"document\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Indicates the model should return text.\",\"Indicates the model should return images.\",\"Indicates the model should return audio.\",\"Indicates the model should return video.\",\"Indicates the model should return documents.\"],\"x-speakeasy-model-namespace\":\"interactions\"},\"Retrieval\":{\"description\":\"A tool that can be used by the model to retrieve files.\",\"properties\":{\"exa_ai_search_config\":{\"$ref\":\"#/$defs/ExaAISearchConfig\",\"description\":\"Used to specify configuration for ExaAISearch.\"},\"parallel_ai_search_config\":{\"$ref\":\"#/$defs/ParallelAISearchConfig\",\"description\":\"Used to specify configuration for ParallelAISearch.\"},\"rag_store_config\":{\"$ref\":\"#/$defs/RagStoreConfig\",\"description\":\"Used to specify configuration for RagStore.\"},\"retrieval_types\":{\"description\":\"The types of file retrieval to enable.\",\"items\":{\"enum\":[\"vertex_ai_search\",\"rag_store\",\"exa_ai_search\",\"parallel_ai_search\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"\",\"\",\"\",\"\"]},\"type\":\"array\"},\"type\":{\"const\":\"retrieval\"},\"vertex_ai_search_config\":{\"$ref\":\"#/$defs/VertexAISearchConfig\",\"description\":\"Used to specify configuration for VertexAISearch.\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"RetrievalCallStep\":{\"description\":\"Retrieval call step.\\nUsed by Vertex Retrieval tools such as Parallel AI, Exa AI, Vertex AI Search,\\netc. RetrievalType decides which tool is used.\",\"properties\":{\"arguments\":{\"$ref\":\"#/$defs/RetrievalStepArguments\",\"allOf\":[{\"$ref\":\"#/$defs/RetrievalStepArguments\"}],\"description\":\"Required. The arguments to pass to the retrieval tool.\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"retrieval_type\":{\"description\":\"The type of retrieval tools.\",\"enum\":[\"vertex_ai_search\",\"rag_store\",\"exa_ai_search\",\"parallel_ai_search\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"\",\"\",\"\",\"\"]},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"retrieval_call\"\x7d\x7d,\"required\":[\"arguments\",\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"RetrievalCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"RetrievalResultStep\":{\"description\":\"Vertex Retrieval result step.\\nUsed by Vertex Retrieval tools such as Parallel AI, Exa AI, Vertex AI Search,\\netc.\",\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"is_error\":{\"description\":\"Whether the retrieval resulted in an error.\",\"type\":\"boolean\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"retrieval_result\"\x7d\x7d,\"required\":[\"call_id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"RetrievalResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"RetrievalStepArguments\":{\"description\":\"The arguments to pass to Retrieval tools.\",\"properties\":{\"queries\":{\"description\":\"Queries for Retrieval information.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"RetrievalCallArguments\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"RetrievalCallArguments\"},\"ReviewSnippet\":{\"description\":\"Encapsulates a snippet of a user review that answers a question about\\nthe features of a specific place in Google Maps.\",\"properties\":{\"review_id\":{\"description\":\"The ID of the review snippet.\",\"type\":\"string\"},\"title\":{\"description\":\"Title of the review.\",\"type\":\"string\"},\"url\":{\"description\":\"A link that corresponds to the user review on Google Maps.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"SafetySetting\":{\"description\":\"A safety setting that affects the safety-blocking behavior.\\n\\nA SafetySetting consists of a\\nharm category and a\\nthreshold for that\\ncategory.\",\"properties\":{\"method\":{\"description\":\"Optional. The method for blocking content. If not specified, the default\\nbehavior is to use the probability score.\",\"enum\":[\"severity\",\"probability\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"The harm block method uses both probability and severity scores.\",\"The harm block method uses the probability score.\"]},\"threshold\":{\"description\":\"Required. The threshold for blocking content. If the harm probability\\nexceeds this threshold, the content will be blocked.\",\"enum\":[\"block_low_and_above\",\"block_medium_and_above\",\"block_only_high\",\"block_none\",\"off\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Block content with a low harm probability or higher.\",\"Block content with a medium harm probability or higher.\",\"Block content with a high harm probability.\",\"Do not block any content, regardless of its harm probability.\",\"Turn off the safety filter entirely.\"]},\"type\":{\"$ref\":\"#/$defs/HarmCategory\",\"description\":\"Required. The type of harm category to be blocked.\"\x7d\x7d,\"required\":[\"threshold\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"SafetySetting\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ServiceTier\":{\"enum\":[\"flex\",\"standard\",\"priority\",\"deferred\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Flex service tier.\",\"Standard service tier.\",\"Priority service tier.\",\"Deferred service tier.\"],\"x-speakeasy-model-namespace\":\"interactions\"},\"SessionConfig\":{\"description\":\"The configuration of CodeMender sessions.\",\"properties\":{\"max_rounds\":{\"description\":\"The maximum number of interaction rounds the agent is allowed to perform\\nbefore reaching a timeout.\",\"format\":\"int32\",\"type\":\"integer\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"SmartTranscriptionMode\":{\"description\":\"Configuration for smart transcription mode.\",\"properties\":{\"type\":{\"const\":\"smart\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Source\":{\"description\":\"A source to be mounted into the environment.\",\"properties\":{\"content\":{\"description\":\"The inline content if `type` is `INLINE`.\",\"type\":\"string\"},\"encoding\":{\"description\":\"Optional encoding for inline content (e.g. `base64`).\",\"type\":\"string\"},\"source\":{\"description\":\"The source of the environment.\\nFor Cloud Storage, this is the Cloud Storage path.\\nFor GitHub, this is the GitHub path.\",\"type\":\"string\"},\"target\":{\"description\":\"Where the source should appear in the environment.\",\"type\":\"string\"},\"type\":{\"enum\":[\"gcs\",\"inline\",\"repository\",\"skill_registry\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"A Cloud Storage bucket.\",\"Inline content.\",\"A generic repository. The protocol prefix in the source URL\\nidentifies the provider (e.g., github://, gcs://).\",\"A skill resource from the Skill Registry Service.\\nSkill: projects/{project}/locations/{location}/skills/{skill}\\nSkillRevision:\\nprojects/{project}/locations/{location}/skills/{skill}/revisions/{revision}\\nSupport mounting all skills under a project:\\nprojects/{project}/locations/{location}/skills.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"SpeakerConfig\":{\"description\":\"Configuration for multi-speaker and speech generation.\",\"properties\":{\"speakers\":{\"description\":\"Individual speaker configurations.\",\"items\":{\"$ref\":\"#/$defs/SpeechConfig\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"SpeechConfig\":{\"description\":\"The configuration for speech interaction.\",\"properties\":{\"language\":{\"description\":\"The language of the speech.\",\"type\":\"string\"},\"speaker\":{\"description\":\"The speaker's name, it should match the speaker name given in the prompt.\",\"type\":\"string\"},\"voice\":{\"description\":\"The voice of the speaker.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"SpeechConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"StaticMediaProcessing\":{\"properties\":{\"end_offset\":{\"description\":\"Optional. Segment end time. Specified as a decimal number of seconds followed\\nby an 's' suffix, e.g., \\\"30s\\\". Must be non-negative and greater than\\n`start_offset` if `start_offset` is set.\",\"format\":\"google-duration\",\"type\":\"string\"},\"fps\":{\"description\":\"Optional. Video frame-rate sampling density.\",\"format\":\"double\",\"type\":\"number\"},\"start_offset\":{\"description\":\"Optional. Segment start time. Specified as a decimal number of seconds followed\\nby an 's' suffix, e.g., \\\"10.5s\\\". Must be non-negative.\",\"format\":\"google-duration\",\"type\":\"string\"},\"type\":{\"const\":\"static\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Status\":{\"description\":\"The `Status` type defines a logical error model that is suitable for\\ndifferent programming environments, including REST APIs and RPC APIs. It is\\nused by [gRPC](https://github.com/grpc). Each `Status` message contains\\nthree pieces of data: error code, error message, and error details.\\n\\nYou can find out more about this error model and how to work with it in the\\n[API Design Guide](https://cloud.google.com/apis/design/errors).\",\"properties\":{\"code\":{\"description\":\"The status code, which should be an enum value of google.rpc.Code.\",\"format\":\"int32\",\"type\":\"integer\"},\"details\":{\"description\":\"A list of messages that carry the error details. There is a common set of\\nmessage types for APIs to use.\",\"items\":{\"additionalProperties\":{\"description\":\"Properties of the object. Contains field @type with type URL.\"},\"type\":\"object\"},\"type\":\"array\"},\"message\":{\"description\":\"A developer-facing error message, which should be in English. Any\\nuser-facing error message should be localized and sent in the\\ngoogle.rpc.Status.details field, or localized by the client.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Step\":{\"description\":\"A step in the interaction.\",\"oneOf\":[{\"$ref\":\"#/$defs/CodeExecutionCallStep\"},{\"$ref\":\"#/$defs/CodeExecutionResultStep\"},{\"$ref\":\"#/$defs/FileSearchCallStep\"},{\"$ref\":\"#/$defs/FileSearchResultStep\"},{\"$ref\":\"#/$defs/FunctionCallStep\"},{\"$ref\":\"#/$defs/FunctionResultStep\"},{\"$ref\":\"#/$defs/GoogleMapsCallStep\"},{\"$ref\":\"#/$defs/GoogleMapsResultStep\"},{\"$ref\":\"#/$defs/GoogleSearchCallStep\"},{\"$ref\":\"#/$defs/GoogleSearchResultStep\"},{\"$ref\":\"#/$defs/McpServerToolCallStep\"},{\"$ref\":\"#/$defs/McpServerToolResultStep\"},{\"$ref\":\"#/$defs/ModelOutputStep\"},{\"$ref\":\"#/$defs/ProcessingCallStep\"},{\"$ref\":\"#/$defs/ProcessingResultStep\"},{\"$ref\":\"#/$defs/RetrievalCallStep\"},{\"$ref\":\"#/$defs/RetrievalResultStep\"},{\"$ref\":\"#/$defs/ThoughtStep\"},{\"$ref\":\"#/$defs/UrlContextCallStep\"},{\"$ref\":\"#/$defs/UrlContextResultStep\"},{\"$ref\":\"#/$defs/UserInputStep\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Step\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"TextContent\":{\"description\":\"A text content block.\",\"examples\":[{\"text\":{\"summary\":\"Text\",\"value\":{\"text\":\"Hello, how are you?\",\"type\":\"text\"\x7d\x7d}],\"properties\":{\"annotations\":{\"description\":\"Citation information for model-generated content.\",\"items\":{\"$ref\":\"#/$defs/Annotation\"},\"type\":\"array\"},\"text\":{\"description\":\"Required. The text content.\",\"type\":\"string\"},\"type\":{\"const\":\"text\"\x7d\x7d,\"required\":[\"text\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"TextContent\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"TextResponseFormat\":{\"description\":\"Configuration for text output format.\",\"examples\":[{\"text_response_format\":{\"summary\":\"Text Output (JSON Schema)\",\"value\":{\"mime_type\":\"application/json\",\"schema\":{\"properties\":{\"ingredients\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"recipe_name\":{\"type\":\"string\"\x7d\x7d,\"required\":[\"ingredients\",\"recipe_name\"],\"type\":\"object\"},\"type\":\"text\"\x7d\x7d}],\"properties\":{\"mime_type\":{\"description\":\"The MIME type of the text output.\",\"enum\":[\"application/json\",\"text/plain\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"JSON output format.\",\"Plain text output format.\"]},\"schema\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"The JSON schema that the output should conform to. Only applicable when\\nmime_type is application/json.\",\"type\":\"object\"},\"type\":{\"const\":\"text\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"TextResponseFormat\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ThinkingLevel\":{\"enum\":[\"minimal\",\"low\",\"medium\",\"high\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Little to no thinking.\",\"Low thinking level.\",\"Medium thinking level.\",\"High thinking level.\"],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ThinkingLevel\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ThinkingSummaries\":{\"enum\":[\"auto\",\"none\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Auto thinking summaries.\",\"No thinking summaries.\"],\"x-speakeasy-model-namespace\":\"interactions\"},\"ThoughtStep\":{\"description\":\"A thought step.\",\"examples\":[{\"thought\":{\"summary\":\"ThoughtStep\",\"value\":{\"signature\":\"thought_sig_abcd1234\",\"summary\":[{\"text\":\"The model is searching Google for the capital of France.\",\"type\":\"text\"}],\"type\":\"thought\"\x7d\x7d}],\"properties\":{\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"summary\":{\"description\":\"A summary of the thought.\",\"items\":{\"$ref\":\"#/$defs/ThoughtSummaryContent\"},\"type\":\"array\"},\"type\":{\"const\":\"thought\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ThoughtStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ThoughtSummaryContent\":{\"oneOf\":[{\"$ref\":\"#/$defs/ImageContent\"},{\"$ref\":\"#/$defs/TextContent\"}],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"Tool\":{\"description\":\"A tool that can be used by the model.\",\"oneOf\":[{\"$ref\":\"#/$defs/CodeExecution\"},{\"$ref\":\"#/$defs/ComputerUse\"},{\"$ref\":\"#/$defs/FileSearch\"},{\"$ref\":\"#/$defs/Function\"},{\"$ref\":\"#/$defs/GoogleMaps\"},{\"$ref\":\"#/$defs/GoogleSearch\"},{\"$ref\":\"#/$defs/McpServer\"},{\"$ref\":\"#/$defs/Retrieval\"},{\"$ref\":\"#/$defs/UrlContext\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"Tool\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ToolChoiceConfig\":{\"description\":\"The tool choice configuration containing allowed tools.\",\"example\":{\"allowed_tools\":{\"mode\":\"any\",\"tools\":[\"my_tool\"]\x7d\x7d,\"properties\":{\"allowed_tools\":{\"$ref\":\"#/$defs/AllowedTools\",\"description\":\"The allowed tools.\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ToolChoiceConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"ToolChoiceType\":{\"enum\":[\"auto\",\"any\",\"none\",\"validated\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Auto tool choice.\",\"Any tool choice.\",\"No tool choice.\",\"Validated tool choice.\"],\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"ToolChoiceType\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"TranscriptionConfig\":{\"description\":\"Configuration for speech recognition (transcription).\",\"properties\":{\"adaptation_phrases\":{\"deprecated\":true,\"description\":\"Optional. A list of phrases to bias the ASR model towards.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"custom_vocabulary\":{\"description\":\"Optional. A list of custom vocabulary phrases to bias the speech recognition model\\ntoward recognizing specific terms.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"diarization_mode\":{\"deprecated\":true,\"description\":\"Optional. Configures speaker diarization. Supported values: \\\"speaker\\\".\",\"type\":\"string\"},\"language_codes\":{\"description\":\"Optional. BCP-47 language codes providing hints about the languages present in the\\naudio. If omitted or empty, defaults to automatic language detection.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"mode\":{\"description\":\"Discriminated transcription mode options or enum.\",\"oneOf\":[{\"$ref\":\"#/$defs/TranscriptionMode\"},{\"enum\":[\"verbatim\",\"smart\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Verbatim transcription mode.\",\"Smart transcription mode.\"]}],\"title\":\"TranscriptionConfigMode\"},\"timestamp_granularities\":{\"deprecated\":true,\"description\":\"Optional. The granularity of timestamps to include in the transcription output.\\nSupported values: \\\"word\\\". If empty, no timestamps are generated.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"TranscriptionConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"TranscriptionMode\":{\"description\":\"Configuration for transcription mode.\",\"oneOf\":[{\"$ref\":\"#/$defs/SmartTranscriptionMode\"},{\"$ref\":\"#/$defs/VerbatimTranscriptionMode\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"TranscriptionMode\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"UrlCitation\":{\"description\":\"A URL citation annotation.\",\"properties\":{\"end_index\":{\"description\":\"End of the attributed segment, exclusive.\",\"format\":\"int32\",\"type\":\"integer\"},\"start_index\":{\"description\":\"Start of segment of the response that is attributed to this source.\\n\\nIndex indicates the start of the segment, measured in bytes.\",\"format\":\"int32\",\"type\":\"integer\"},\"title\":{\"description\":\"The title of the URL.\",\"type\":\"string\"},\"type\":{\"const\":\"url_citation\"},\"url\":{\"description\":\"The URL.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"URLCitation\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLCitation\"},\"UrlContext\":{\"description\":\"A tool that can be used by the model to fetch URL context.\",\"properties\":{\"type\":{\"const\":\"url_context\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-codeSamples\":[{\"label\":\"url_context\",\"lang\":\"sh\",\"source\":\"curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\\\\n -H \\\"x-goog-api-key: $GEMINI_API_KEY\\\" \\\\\\n -H \\\"Content-Type: application/json\\\" \\\\\\n -d '{\\n \\\"model\\\": \\\"gemini-3.6-flash\\\",\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"url_context\\\"\\n }],\\n \\\"input\\\": \\\"Summarize https://www.example.com\\\"\\n }'\\n\"},{\"label\":\"url_context\",\"lang\":\"python\",\"source\":\"from google import genai\\n\\nclient = genai.Client()\\nresponse = client.interactions.create(\\n model=\\\"gemini-3.6-flash\\\",\\n tools=[{\\\"type\\\": \\\"url_context\\\"}],\\n input=\\\"Summarize https://www.example.com\\\"\\n)\\nprint(response.output_text)\\n\"},{\"label\":\"url_context\",\"lang\":\"javascript\",\"source\":\"import {GoogleGenAI} from '@google/genai';\\n\\nconst ai = new GoogleGenAI({});\\nconst interaction = await ai.interactions.create({\\n model: 'gemini-3.6-flash',\\n tools: [{ type: 'url_context' }],\\n input: 'Summarize https://www.example.com'\\n});\\nconsole.log(interaction.output_text);\\n\"},{\"label\":\"url_context\",\"lang\":\"java\",\"source\":\"import com.google.genai.Client;\\nimport com.google.genai.gaos.models.interactions.CreateModelInteraction;\\nimport com.google.genai.gaos.models.interactions.Interaction;\\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\\nimport com.google.genai.gaos.models.interactions.URLContext;\\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\\nimport java.util.List;\\n\\nClient client = new Client();\\nCreateModelInteraction params =\\n CreateModelInteraction.builder()\\n .model(\\\"gemini-3.6-flash\\\")\\n .tools(List.of(new URLContext()))\\n .input(InteractionsInput.of(\\\"Summarize https://www.example.com\\\"))\\n .build();\\nCreateInteractionResponse response =\\n client.interactions.create(CreateInteractionRequestBody.of(params));\\nInteraction interaction =\\n response.interaction().orElseThrow(() -\\u003e new RuntimeException(\\\"No interaction returned\\\"));\\nSystem.out.println(interaction.outputText().orElse(\\\"\\\"));\\n\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLContext\"},\"UrlContextCallArguments\":{\"description\":\"The arguments to pass to the URL context.\",\"properties\":{\"urls\":{\"description\":\"The URLs to fetch.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"URLContextCallArguments\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLContextCallArguments\"},\"UrlContextCallStep\":{\"description\":\"URL context call step.\",\"examples\":[{\"url_context_call\":{\"summary\":\"UrlContextCallStep\",\"value\":{\"arguments\":{\"urls\":[\"https://www.example.com\"]},\"id\":\"url_call_10219\",\"type\":\"url_context_call\"\x7d\x7d}],\"properties\":{\"arguments\":{\"$ref\":\"#/$defs/UrlContextCallArguments\",\"description\":\"Required. The arguments to pass to the URL context.\"},\"id\":{\"description\":\"Required. A unique ID for this specific tool call.\",\"type\":\"string\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"url_context_call\"\x7d\x7d,\"required\":[\"arguments\",\"id\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"URLContextCallStep\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLContextCallStep\"},\"UrlContextResult\":{\"description\":\"The result of the URL context.\",\"properties\":{\"status\":{\"description\":\"The status of the URL retrieval.\",\"enum\":[\"success\",\"error\",\"paywall\",\"unsafe\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Url retrieval is successful.\",\"Url retrieval is failed due to error.\",\"Url retrieval is failed because the content is behind paywall.\",\"Url retrieval is failed because the content is unsafe.\"]},\"url\":{\"description\":\"The URL that was fetched.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"URLContextResult\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLContextResult\"},\"UrlContextResultStep\":{\"description\":\"URL context result step.\",\"examples\":[{\"url_context_result\":{\"summary\":\"UrlContextResultStep\",\"value\":{\"call_id\":\"url_call_10219\",\"result\":[{\"snippet\":\"This domain is for use in illustrative examples in documents.\",\"title\":\"Example Domain\",\"url\":\"https://www.example.com\"}],\"type\":\"url_context_result\"\x7d\x7d}],\"properties\":{\"call_id\":{\"description\":\"Required. ID to match the ID from the function call block.\",\"type\":\"string\"},\"is_error\":{\"description\":\"Whether the URL context resulted in an error.\",\"type\":\"boolean\"},\"result\":{\"description\":\"Required. The results of the URL context.\",\"items\":{\"$ref\":\"#/$defs/UrlContextResult\"},\"type\":\"array\"},\"signature\":{\"description\":\"A signature hash for backend validation.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"type\":{\"const\":\"url_context_result\"\x7d\x7d,\"required\":[\"call_id\",\"result\",\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"URLContextResultStep\"}],\"x-speakeasy-model-namespace\":\"interactions\",\"x-speakeasy-name-override\":\"URLContextResultStep\"},\"UserInputStep\":{\"description\":\"Input provided by the user.\",\"examples\":[{\"user_input\":{\"summary\":\"UserInputStep\",\"value\":{\"content\":[{\"text\":\"What is the capital of France?\",\"type\":\"text\"}],\"type\":\"user_input\"\x7d\x7d}],\"properties\":{\"content\":{\"items\":{\"$ref\":\"#/$defs/Content\"},\"title\":\"ContentList\",\"type\":\"array\"},\"type\":{\"const\":\"user_input\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"UserInputStep\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"VerbatimTranscriptionMode\":{\"description\":\"Configuration for verbatim transcription mode.\",\"properties\":{\"diarization_mode\":{\"description\":\"Optional. Configures speaker diarization. Supported values: \\\"speaker\\\".\",\"type\":\"string\"},\"timestamp_granularities\":{\"description\":\"Optional. The granularity of timestamps to include in the transcription output.\\nSupported values: \\\"word\\\". If empty, no timestamps are generated.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"type\":{\"const\":\"verbatim\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"VertexAISearchConfig\":{\"description\":\"Used to specify configuration for VertexAISearch.\",\"properties\":{\"datastores\":{\"description\":\"Optional. Used to specify Vertex AI Search datastores.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"engine\":{\"description\":\"Optional. Used to specify Vertex AI Search engine.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"interactions\"},\"VideoConfig\":{\"description\":\"Configuration options for video generation.\",\"properties\":{\"task\":{\"description\":\"Optional task mode for video generation. If not specified, the model\\nautomatically determines the appropriate mode based on the provided text\\nprompt and input media.\",\"enum\":[\"text_to_video\",\"image_to_video\",\"reference_to_video\",\"edit\",\"extend\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Generates video solely from a text prompt.\",\"Generates video from one or two source images. The first image defines\\nthe starting frame, and the optional second image defines the ending\\nframe.\",\"Generates video using reference media (such as images, audio, or video).\",\"Modifies an existing input video.\",\"Extends an existing input video.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"VideoConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"VideoContent\":{\"description\":\"A video content block.\",\"examples\":[{\"video\":{\"summary\":\"Video\",\"value\":{\"type\":\"video\",\"uri\":\"https://www.youtube.com/watch?v=9hE5-98ZeCg\"\x7d\x7d}],\"properties\":{\"data\":{\"description\":\"The video content.\",\"format\":\"byte\",\"type\":\"string\",\"x-speakeasy-base64-input-mode\":\"file\"},\"mime_type\":{\"description\":\"The mime type of the video.\",\"enum\":[\"video/mp4\",\"video/mpeg\",\"video/mpg\",\"video/mov\",\"video/avi\",\"video/x-flv\",\"video/webm\",\"video/wmv\",\"video/3gpp\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"MP4 video format\",\"MPEG video format\",\"MPG video format\",\"MOV video format\",\"AVI video format\",\"FLV video format\",\"WebM video format\",\"WMV video format\",\"3GPP video format\"]},\"name\":{\"description\":\"A user-defined name for this content block. Can be referenced by the model\\nin the final response.\",\"type\":\"string\"},\"processing\":{\"description\":\"How the model processes this video for understanding.\",\"oneOf\":[{\"$ref\":\"#/$defs/MediaProcessing\"},{\"enum\":[\"static\",\"agentic\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Fixed-rate frame extraction. All frames placed in context.\",\"Model-driven dynamic navigation.\"]}]},\"resolution\":{\"$ref\":\"#/$defs/MediaResolution\",\"description\":\"The resolution of the media.\"},\"type\":{\"const\":\"video\"},\"uri\":{\"description\":\"The URI of the video.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"VideoContent\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"VideoResponseFormat\":{\"description\":\"Configuration for video output format.\",\"examples\":[{\"video_response_format\":{\"summary\":\"Video Output\",\"value\":{\"aspect_ratio\":\"16:9\",\"delivery\":\"inline\",\"type\":\"video\"\x7d\x7d}],\"properties\":{\"aspect_ratio\":{\"description\":\"The aspect ratio for the video output.\",\"enum\":[\"16:9\",\"9:16\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"16:9 aspect ratio.\",\"9:16 aspect ratio.\"]},\"delivery\":{\"description\":\"The delivery mode for the video output.\",\"enum\":[\"inline\",\"uri\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Video data is returned inline in the response.\",\"Video data is returned as a URI.\"]},\"duration\":{\"description\":\"The duration for the video output.\",\"format\":\"google-duration\",\"type\":\"string\"},\"gcs_uri\":{\"description\":\"The Cloud Storage URI to store the video output. Required for Vertex if\\ndelivery mode is URI.\",\"type\":\"string\"},\"resolution\":{\"description\":\"The video output resolution. Defaults to 720p.\",\"enum\":[\"360p\",\"720p\",\"1080p\",\"4k\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"360p resolution.\",\"720p resolution.\",\"1080p resolution.\",\"4K resolution.\"]},\"type\":{\"const\":\"video\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"VideoResponseFormat\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"WebhookConfig\":{\"description\":\"Message for configuring webhook events for a request.\",\"properties\":{\"uris\":{\"description\":\"Optional. If set, these webhook URIs will be used for webhook events instead of the\\nregistered webhooks.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"user_metadata\":{\"additionalProperties\":{\"description\":\"Properties of the object.\"},\"description\":\"Optional. The user metadata that will be returned on each event emission to the\\nwebhooks.\",\"type\":\"object\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"WebhookConfig\"}],\"x-speakeasy-model-namespace\":\"interactions\"},\"WordInfo\":{\"description\":\"Word-level ASR annotation for transcription output.\\nCarries the word text, optional timing, and optional speaker attribution.\",\"properties\":{\"end_index\":{\"description\":\"End of the attributed segment, exclusive.\",\"format\":\"int32\",\"type\":\"integer\"},\"end_offset\":{\"description\":\"End offset in time of the word relative to the start of the audio.\\nPresent when timestamp_granularities contains \\\"word\\\".\",\"format\":\"google-duration\",\"type\":\"string\"},\"speaker\":{\"description\":\"Optional. Speaker label for this word (e.g. \\\"spk_1\\\", \\\"spk_2\\\").\\nPresent when diarization_mode is set in TranscriptionConfig.\",\"type\":\"string\"},\"start_index\":{\"description\":\"Start of segment of the response that is attributed to this source.\\n\\nIndex indicates the start of the segment, measured in bytes.\",\"format\":\"int32\",\"type\":\"integer\"},\"start_offset\":{\"description\":\"Start offset in time of the word relative to the start of the audio.\\nPresent when timestamp_granularities contains \\\"word\\\".\",\"format\":\"google-duration\",\"type\":\"string\"},\"text\":{\"description\":\"The transcribed word.\",\"type\":\"string\"},\"type\":{\"const\":\"word_info\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"interactions\",\"name\":\"WordInfo\"}],\"x-speakeasy-model-namespace\":\"interactions\"\x7d\x7d,\"oneOf\":[{\"$ref\":\"#/$defs/CreateAgentInteractionParams\"},{\"$ref\":\"#/$defs/CreateModelInteractionParams\"}]}", + "CreateWebhook": "{\"$defs\":{\"SigningSecret\":{\"description\":\"Represents a signing secret used to verify webhook payloads.\",\"properties\":{\"expire_time\":{\"description\":\"Output only. The expiration date of the signing secret.\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"truncated_secret\":{\"description\":\"Output only. The truncated version of the signing secret.\",\"readOnly\":true,\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"webhooks\",\"name\":\"SigningSecret\"}],\"x-speakeasy-model-namespace\":\"webhooks\"},\"Webhook\":{\"description\":\"A Webhook resource.\",\"properties\":{\"create_time\":{\"description\":\"Output only. The timestamp when the webhook was created.\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"id\":{\"description\":\"Output only. The ID of the webhook.\",\"readOnly\":true,\"type\":\"string\"},\"name\":{\"description\":\"Optional. The user-provided name of the webhook.\",\"type\":\"string\"},\"new_signing_secret\":{\"description\":\"Output only. The new signing secret for the webhook. Only populated on create.\",\"readOnly\":true,\"type\":\"string\"},\"signing_secrets\":{\"description\":\"Output only. The signing secrets associated with this webhook.\",\"items\":{\"$ref\":\"#/$defs/SigningSecret\"},\"readOnly\":true,\"type\":\"array\"},\"state\":{\"description\":\"Output only. The state of the webhook.\",\"enum\":[\"enabled\",\"disabled\",\"disabled_due_to_failed_deliveries\"],\"readOnly\":true,\"type\":\"string\",\"x-google-enum-descriptions\":[\"The webhook is enabled.\",\"The webhook is disabled by the user.\",\"The webhook is disabled due to failed deliveries.\"]},\"subscribed_events\":{\"description\":\"Required. The events that the webhook is subscribed to.\\nAvailable events:\\n- batch.succeeded\\n- batch.expired\\n- batch.failed\\n- interaction.requires_action\\n- interaction.completed\\n- interaction.failed\\n- video.generated\",\"items\":{\"enum\":[\"batch.succeeded\",\"batch.expired\",\"batch.failed\",\"interaction.requires_action\",\"interaction.completed\",\"interaction.failed\",\"video.generated\"],\"type\":\"string\",\"x-speakeasy-enum-descriptions\":[\"Batch processing finished successfully.\",\"Batch has not been processed within the 48h timeframe.\",\"Batch job failed.\",\"Interaction requires action (e.g., function calling).\",\"Interaction completed successfully.\",\"Interaction failed.\",\"Video generation completed.\"],\"x-speakeasy-enum-format\":\"union\",\"x-speakeasy-unknown-values\":\"allow\"},\"type\":\"array\"},\"update_time\":{\"description\":\"Output only. The timestamp when the webhook was last updated.\",\"format\":\"date-time\",\"readOnly\":true,\"type\":\"string\"},\"uri\":{\"description\":\"Required. The URI to which webhook events will be sent.\",\"type\":\"string\"\x7d\x7d,\"required\":[\"subscribed_events\",\"uri\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"webhooks\",\"name\":\"Webhook\"},{\"group\":\"webhooks\",\"name\":\"WebhookInput\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"webhooks\"\x7d\x7d,\"$ref\":\"#/$defs/Webhook\"}", + "FilesRegister": "{\"$defs\":{\"GenAIRegisterFilesRequest\":{\"description\":\"Request for `RegisterFiles`.\",\"properties\":{\"uris\":{\"description\":\"Required. The Google Cloud Storage URIs to register. Example: `gs://bucket/object`.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-model-namespace\":\"genai\",\"x-speakeasy-name-override\":\"RegisterFilesRequest\"\x7d\x7d,\"$ref\":\"#/$defs/GenAIRegisterFilesRequest\"}", + "PingWebhook": "{\"$defs\":{\"PingWebhookRequest\":{\"description\":\"Request message for WebhookService.PingWebhook.\",\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"webhooks\",\"name\":\"WebhookPingParams\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"webhooks\",\"x-stainless-empty-object\":true\x7d\x7d,\"$ref\":\"#/$defs/PingWebhookRequest\"}", + "RotateSigningSecret": "{\"$defs\":{\"RotateSigningSecretRequest\":{\"description\":\"Request message for WebhookService.RotateSigningSecret.\",\"properties\":{\"revocation_behavior\":{\"description\":\"Optional. The revocation behavior for previous signing secrets.\",\"enum\":[\"revoke_previous_secrets_after_h24\",\"revoke_previous_secrets_immediately\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Generate a new signing secret and revoke all previous secrets after 24\\nhours. Default and safest option for migrations.\",\"Revoke all previous secrets immediately. Use with caution as this can\\ninterrupt ongoing notifications.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"webhooks\",\"name\":\"WebhookRotateSigningSecretParams\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"webhooks\"\x7d\x7d,\"$ref\":\"#/$defs/RotateSigningSecretRequest\"}", + "UpdateCredential": "{\"$defs\":{\"CredentialUpdateParams\":{\"description\":\"Represents the fields of a Credential that can be updated.\",\"oneOf\":[{\"$ref\":\"#/$defs/EnvironmentVariableUpdateConfig\"},{\"$ref\":\"#/$defs/HttpBearerUpdateConfig\"},{\"$ref\":\"#/$defs/OAuth2UpdateConfig\"}],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"CredentialUpdate\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"credentials\",\"x-speakeasy-name-override\":\"CredentialUpdate\"},\"EnvironmentVariableUpdateConfig\":{\"description\":\"Configuration for updating environment variable credentials.\",\"properties\":{\"injection_location\":{\"description\":\"Optional. Locations where the environment variable can be injected in\\noutgoing HTTP requests.\\nAccepts either a single location (e.g. \\\"header\\\") or an array of locations.\",\"oneOf\":[{\"$ref\":\"#/$defs/InjectionLocation\"},{\"items\":{\"$ref\":\"#/$defs/InjectionLocation\"},\"type\":\"array\"}]},\"trusted_domains\":{\"description\":\"Optional. List of domains allowed to receive this environment variable\\nvalue in HTTP requests.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"type\":{\"const\":\"environment_variable\"},\"value\":{\"description\":\"Optional. Input only. Secret value of the environment variable. Write-only; never\\nreturned in responses.\",\"type\":\"string\",\"writeOnly\":true\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"EnvironmentVariableUpdateConfig\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"HttpBearerUpdateConfig\":{\"description\":\"Configuration for updating HTTP Bearer token credentials.\",\"properties\":{\"header_name\":{\"description\":\"Optional. Header name to inject the token into. Defaults to\\n'Authorization'.\",\"type\":\"string\"},\"prefix\":{\"description\":\"Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\\nfor no prefix.\",\"type\":\"string\"},\"token\":{\"description\":\"Optional. Input only. The static bearer token. Write-only; never returned in responses.\",\"type\":\"string\",\"writeOnly\":true},\"type\":{\"const\":\"bearer_token\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"HttpBearerUpdateConfig\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"InjectionLocation\":{\"enum\":[\"header\",\"query\",\"body\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"Injected into HTTP request headers.\",\"Injected into HTTP URL query parameters.\",\"Injected into HTTP request body.\"],\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"InjectionLocation\"}],\"x-speakeasy-model-namespace\":\"credentials\"},\"OAuth2UpdateConfig\":{\"description\":\"Configuration for updating OAuth2 credentials.\",\"properties\":{\"client_id\":{\"description\":\"Optional. OAuth2 client ID.\",\"type\":\"string\"},\"client_secret\":{\"description\":\"Optional. Input only. OAuth2 client secret. Write-only; never returned in responses.\",\"type\":\"string\",\"writeOnly\":true},\"refresh_token\":{\"description\":\"Optional. Input only. OAuth2 refresh token. Write-only; never returned in responses.\",\"type\":\"string\",\"writeOnly\":true},\"scopes\":{\"description\":\"Optional. List of OAuth2 scopes.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"token_url\":{\"description\":\"Optional. OAuth2 token endpoint URL for refreshing access tokens.\",\"type\":\"string\"},\"type\":{\"const\":\"oauth2\"\x7d\x7d,\"required\":[\"type\"],\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"credentials\",\"name\":\"OAuth2UpdateConfig\"}],\"x-speakeasy-model-namespace\":\"credentials\"\x7d\x7d,\"$ref\":\"#/$defs/CredentialUpdateParams\"}", + "UpdateTrigger": "{\"$defs\":{\"TriggerUpdate\":{\"description\":\"Represents the fields of a Trigger that can be updated.\",\"properties\":{\"display_name\":{\"description\":\"Optional. The display name of the trigger.\",\"type\":\"string\"},\"status\":{\"description\":\"Optional. The status of the trigger.\",\"enum\":[\"active\",\"paused\",\"error\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"The trigger is active and will fire on schedule.\",\"The trigger is paused and will not fire.\",\"The trigger has entered an error state due to consecutive failures.\"]\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"triggers\",\"name\":\"TriggerUpdate\"}],\"x-speakeasy-model-namespace\":\"triggers\"\x7d\x7d,\"$ref\":\"#/$defs/TriggerUpdate\"}", + "UpdateWebhook": "{\"$defs\":{\"WebhookUpdate\":{\"properties\":{\"name\":{\"description\":\"Optional. The user-provided name of the webhook.\",\"type\":\"string\"},\"state\":{\"description\":\"Optional. The state of the webhook.\",\"enum\":[\"enabled\",\"disabled\",\"disabled_due_to_failed_deliveries\"],\"type\":\"string\",\"x-google-enum-descriptions\":[\"The webhook is enabled.\",\"The webhook is disabled by the user.\",\"The webhook is disabled due to failed deliveries.\"]},\"subscribed_events\":{\"description\":\"Optional. The events that the webhook is subscribed to.\\nAvailable events:\\n- batch.succeeded\\n- batch.expired\\n- batch.failed\\n- interaction.requires_action\\n- interaction.completed\\n- interaction.failed\\n- video.generated\",\"items\":{\"enum\":[\"batch.succeeded\",\"batch.expired\",\"batch.failed\",\"interaction.requires_action\",\"interaction.completed\",\"interaction.failed\",\"video.generated\"],\"type\":\"string\",\"x-speakeasy-enum-descriptions\":[\"Batch processing finished successfully.\",\"Batch has not been processed within the 48h timeframe.\",\"Batch job failed.\",\"Interaction requires action (e.g., function calling).\",\"Interaction completed successfully.\",\"Interaction failed.\",\"Video generation completed.\"],\"x-speakeasy-enum-format\":\"union\",\"x-speakeasy-unknown-values\":\"allow\"},\"type\":\"array\"},\"uri\":{\"description\":\"Optional. The URI to which webhook events will be sent.\",\"type\":\"string\"\x7d\x7d,\"type\":\"object\",\"x-speakeasy-exports\":[{\"group\":\"webhooks\",\"name\":\"WebhookUpdate\"},{\"group\":\"webhooks\",\"name\":\"WebhookUpdateParams\",\"representation\":\"input\"}],\"x-speakeasy-model-namespace\":\"webhooks\"\x7d\x7d,\"$ref\":\"#/$defs/WebhookUpdate\"}", +} + +func EmitBodySchema(w io.Writer, operationID string) error { + schema, ok := bodySchemas[operationID] + if !ok { + return fmt.Errorf("no request body schema for %s", operationID) + } + _, err := fmt.Fprintln(w, schema) + return err +} diff --git a/internal/usage/schema.go b/internal/usage/schema.go new file mode 100644 index 0000000..64acc20 --- /dev/null +++ b/internal/usage/schema.go @@ -0,0 +1,408 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package usage + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "sync" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var usageSchemas = map[string]string{ + "": "name \"gemini-api\"\nbin \"gemini-api\"\nabout \"\"\"\n Gemini API: Use the Gemini Interactions API and managed-agent platform from the command line.\n\n Get started:\n Set GEMINI_API_KEY, or run: gemini-api configure\n Then run a model or managed agent: gemini-api agent --help\n Add --dry-run to preview any API call without sending it.\n \"\"\"\nversion \"0.6.0\"\nconfig {\n file \"~/.config/gemini-api/config.yaml\"\n}\nflag \"--usage\" help=\"Print the CLI Usage schema in KDL format\" global=#true\nflag \"--help-global\" help=\"Print global flags shared by every command\"\nflag \"-o --output-format \" help=\"Specify the output format. Options: pretty, json, yaml, table, toon.\" global=#true config=\"output_format\" default=\"pretty\"\nflag \"--color \" help=\"Control colored output: auto (color when output is a TTY), always, or never. Respects NO_COLOR and FORCE_COLOR env vars.\" global=#true default=\"auto\"\nflag \"-q --jq \" help=\"Filter and transform output using a jq expression (e.g., '.name', '.items[] | .id')\" global=#true\nflag \"--raw-output\" help=\"Write --jq string results as raw text instead of JSON strings (like jq -r); non-string results stay JSON\" global=#true default=#true\nflag \"--server-url \" help=\"Override the default server URL\" global=#true\nflag \"-H --header \" help=\"Set a custom HTTP request header (format: \\\"Key: Value\\\"). Can be specified multiple times.\" global=#true var=#true\nflag \"--include-headers\" help=\"Include HTTP response headers in the output\" global=#true default=#false\nflag \"--timeout \" help=\"HTTP request timeout (e.g., 30s, 5m, 100ms)\" global=#true config=\"timeout\"\nflag \"--interactive\" help=\"Prompt for missing inputs and open guided configure/auth forms (forms fall back to line prompts on stdin off-TTY)\" global=#true default=#false\nflag \"--no-interactive\" help=\"Disable all interactive features (auto-prompting, explorer auto-launch, TUI forms)\" global=#true default=#false\nflag \"--dry-run\" help=\"Preview API requests without sending them (no network, no OS keychain). Human preview on stderr; with -o json or --jq, one JSON object per request on stdout. Local mutation commands (auth login, auth logout and configure) make no request: they skip prompts and writes and report a no-op (stderr, or one JSON object on stdout in the machine form)\" global=#true default=#false\nflag \"-d --debug\" help=\"Log request and response diagnostics to stderr\" global=#true default=#false\nflag \"--agent-mode\" help=\"Enable structured errors and default TOON output for AI coding agents.\" global=#true default=#false\nflag \"--no-retries\" help=\"Disable automatic retries (default: retries enabled with exponential backoff)\" global=#true config=\"no_retries\" default=#false hide=#true\nflag \"--retry-max-elapsed-time \" help=\"Maximum total time for retries (e.g., 30s, 5m). Default: 30s\" global=#true config=\"retry_max_elapsed_time\" hide=#true\nflag \"--retry-connection-errors\" help=\"Retry on connection errors (EOF, reset, etc.)\" global=#true config=\"retry_connection_errors\" default=#false hide=#true\nflag \"--retry-config \" help=\"Full retry config as JSON. Schema: {\\\"strategy\\\":\\\"backoff\\\",\\\"backoff\\\":{\\\"initialInterval\\\":500,\\\"maxInterval\\\":10000,\\\"exponent\\\":1.5,\\\"maxElapsedTime\\\":30000},\\\"retryConnectionErrors\\\":false}. Use strategy \\\"attempt-count-backoff\\\" with maxRetries for attempt-count retries. Times are in milliseconds.\" global=#true config=\"retry_config\" hide=#true\nflag \"--api-key \" help=\"Gemini API key sent as x-goog-api-key.\" global=#true env=\"GEMINI_API_KEY\" config=\"security.api_key\"\nflag \"--access-token \" help=\"OAuth access token sent as a bearer Authorization header.\" global=#true env=\"GEMINI_ACCESS_TOKEN\" config=\"security.access_token\"\nflag \"--api-version \" help=\"Which version of the API to use\" global=#true env=\"GEMINI_API_VERSION\" config=\"globals.api_version\" default=\"v1beta\"\nflag \"--api-revision \" help=\"Interactions API revision to request\" global=#true env=\"GEMINI_API_REVISION\" config=\"globals.api_revision\"\nflag \"--user-project \" help=\"Quota project header to send with Google GenAI API requests\" global=#true env=\"GEMINI_USER_PROJECT\" config=\"globals.user_project\"\ncmd \"agent\" help=\"Run interactions with Gemini models or managed agents, and manage agent definitions\" {\n cmd \"run\" help=\"Run an interaction with a Gemini model or a managed agent\" {\n arg \"input\" help=\"Prompt or task to send\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--agent \" help=\"Managed agent to run (see \\\"gemini-api agent list\\\") (e.g. deep-research-pro-preview-12-2025, deep-research-preview-04-2026, deep-research-max-preview-04-2026, antigravity-preview-05-2026)\"\n flag \"--background\" help=\"Return immediately with an interaction ID; poll with \\\"gemini-api agent status\\\"\"\n flag \"-m --model \" help=\"Model to run (see \\\"gemini-api models\\\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--stream\" help=\"Stream the reply as it is generated; use --stream=false for one complete interaction (default: true)\"\n }\n cmd \"cancel\" help=\"Cancel an in-progress interaction by interaction ID\" {\n flag \"--id \" help=\"The unique identifier of the interaction to cancel. [required]\"\n }\n cmd \"create\" help=\"Create a managed agent definition\" {\n flag \"--agent-config \" help=\"{ \\\"max_total_tokens\\\": string, \\\"model\\\": string }\"\n flag \"--base-agent \" help=\"The base agent to extend. [required]\"\n flag \"--base-environment \" help=\"{ \\\"env\\\": object, \\\"environment_id\\\": string, \\\"network\\\": object | string | string, \\\"sources\\\": object[] } | string\"\n flag \"--description \" help=\"Agent description for developers to quickly read and understand.\"\n flag \"--id \" help=\"The unique identifier for the agent. [required]\"\n flag \"--system-instruction \" help=\"System instruction for the agent.\"\n flag \"--tools \" help=\"The tools available to the agent.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Delete a managed agent definition by ID\" {\n flag \"--id \" help=\"[required]\"\n }\n cmd \"delete-interaction\" help=\"Delete an interaction by interaction ID\" {\n alias \"di\"\n flag \"--id \" help=\"The unique identifier of the interaction to delete. [required]\"\n }\n cmd \"get\" help=\"Get a managed agent definition by ID\" {\n flag \"--id \" help=\"[required]\"\n }\n cmd \"list\" help=\"List managed agent definitions\" {\n flag \"--page-size \" help=\"integer value\"\n flag \"--page-token \" help=\"string value\"\n flag \"--parent \" help=\"string value\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"status\" help=\"Get status and output of an interaction by interaction ID\" {\n flag \"--id \" help=\"The unique identifier of the interaction to retrieve. [required]\"\n flag \"--include-input\" help=\"If set to true, includes the input in the response.\" default=#false\n flag \"--last-event-id \" help=\"Optional. If set, resumes the interaction stream from the next chunk after the event marked by the event id. Can only be used if `stream` is true.\"\n flag \"--stream\" help=\"Stream the interaction's events (replayed from the start for a finished interaction) instead of returning the status object. Defaults to true; use --stream=false for the status object.\" default=#true\n }\n}\ncmd \"analyze\" help=\"Ask questions about video, audio, PDF, or image files\"\ncmd \"batch\" help=\"Async batch jobs at reduced cost\"\ncmd \"configure\" help=\"Configure authentication, global parameters, and preferences\"\ncmd \"docs\" help=\"Gemini API documentation & guides\"\ncmd \"embed\" help=\"Vector embeddings (gemini-embedding-2)\"\ncmd \"files\" help=\"Upload / list / download / delete media (48h TTL)\" {\n cmd \"delete\" help=\"Deletes the `File`.\" {\n flag \"--file \" help=\"[required]\"\n }\n cmd \"get\" help=\"Gets the metadata for the given `File`.\" {\n flag \"--file \" help=\"[required]\"\n }\n cmd \"list\" help=\"Lists the metadata for `File`s owned by the requesting project.\" {\n flag \"--page-size \" help=\"Optional. Maximum number of `File`s to return per page. If unspecified, defaults to 10. Maximum `page_size` is 100.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous `ListFiles` call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"register\" help=\"Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails.\" {\n flag \"--uris \" help=\"Required. The Google Cloud Storage URIs to register. Example: `gs://bucket/object`.\" var=#true\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\ncmd \"generate\" help=\"Text & multimodal generation (gemini-3.6-flash)\" {\n arg \"prompt\" help=\"Prompt to send to the model\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Model to use (see \\\"gemini-api models\\\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--stream\" help=\"Stream the reply as it is generated; use --stream=false for a single complete result (default: true)\"\n}\ncmd \"image\" help=\"Generate or edit images (gemini-3.1-flash-image)\" {\n arg \"prompt\" help=\"Image prompt\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Override the image model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./gemini-image-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n}\ncmd \"models\" help=\"List available models and the default\" {\n cmd \"get\" help=\"Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information.\" {\n flag \"--model \" help=\"[required]\"\n }\n cmd \"list\" help=\"Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API.\" {\n flag \"--page-size \" help=\"The maximum number of `Models` to return (per page). If unspecified, 50 models will be returned per page. This method returns at most 1000 models per page, even if you pass a larger page_size.\"\n flag \"--page-token \" help=\"A page token, received from a previous `ListModels` call. Provide the `page_token` returned by one request as an argument to the next request to retrieve the next page. When paginating, all other parameters provided to `ListModels` must match the call that provided the page token.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n}\ncmd \"music\" help=\"Music generation (lyria-3-pro-preview)\" {\n arg \"prompt\" help=\"Music prompt\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Override the music model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--out \" help=\"Write the audio to this file (or into this directory). Default: ./gemini-music-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the audio to a file\"\n}\ncmd \"tokens\" help=\"Count tokens without generating\"\ncmd \"transcribe\" help=\"Audio/video → text (timestamps, captions)\"\ncmd \"triggers\" help=\"Schedule and manage cron triggers that run managed agents\" {\n cmd \"delete\" help=\"Delete a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n }\n cmd \"get\" help=\"Get a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n }\n cmd \"list\" help=\"List triggers for a project\" {\n flag \"--filter \" help=\"Optional. Filter expression (e.g., by state).\"\n flag \"--page-size \" help=\"Optional. The maximum number of triggers to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggers call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"list-executions\" help=\"List executions for a trigger\" {\n alias \"le\"\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n flag \"--page-size \" help=\"Optional. The maximum number of executions to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggerExecutions call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"run\" help=\"Run a trigger immediately\" {\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n }\n cmd \"update\" help=\"Update a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n flag \"--display-name \" help=\"Optional. The display name of the trigger.\"\n flag \"--status \" help=\"Optional. The status of the trigger. (options: active, paused, error)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\ncmd \"tts\" help=\"Text to speech (gemini-3.1-flash-tts-preview)\"\ncmd \"video\" help=\"Generate & edit video conversationally (gemini-omni-flash-preview)\" {\n arg \"prompt\" help=\"Video prompt\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Override the video model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--out \" help=\"Write the video to this file (or into this directory). Default: ./gemini-video-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the video to a file\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\ncmd \"webhooks\" help=\"Manage webhook endpoints and signing secrets for event delivery\" {\n cmd \"create\" help=\"Create a webhook endpoint\" {\n flag \"--name \" help=\"Optional. The user-provided name of the webhook.\"\n flag \"--subscribed-events \" help=\"\"\"\n Required. The events that the webhook is subscribed to.\n Available events:\n - batch.succeeded\n - batch.expired\n - batch.failed\n - interaction.requires_action\n - interaction.completed\n - interaction.failed\n - video.generated [required]\n \"\"\" var=#true\n flag \"--uri \" help=\"Required. The URI to which webhook events will be sent. [required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Delete a webhook by ID\" {\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook to delete.\n Format: `{webhook_id}` [required]\n \"\"\"\n }\n cmd \"get\" help=\"Get a webhook by ID\" {\n flag \"--id \" help=\"Required. The ID of the webhook to retrieve. [required]\"\n }\n cmd \"list\" help=\"List webhook endpoints\" {\n flag \"--page-size \" help=\"\"\"\n Optional. The maximum number of webhooks to return. The service may return fewer than\n this value. If unspecified, at most 50 webhooks will be returned.\n The maximum value is 1000.\n \"\"\"\n flag \"--page-token \" help=\"\"\"\n Optional. A page token, received from a previous `ListWebhooks` call.\n Provide this to retrieve the subsequent page.\n \"\"\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"ping\" help=\"Send a ping event to a webhook\" {\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook to ping.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--body-param \" help=\"The request body.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"rotate-signing-secret\" help=\"Rotate the signing secret for a webhook\" {\n alias \"rss\"\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook for which to generate a signing secret.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--revocation-behavior \" help=\"Optional. The revocation behavior for previous signing secrets. (options: revoke_previous_secrets_after_h24, revoke_previous_secrets_immediately)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"update\" help=\"Update a webhook by ID\" {\n flag \"--id \" help=\"Required. The ID of the webhook to update. [required]\"\n flag \"--update-mask \" help=\"Optional. The list of fields to update.\"\n flag \"--name \" help=\"Optional. The user-provided name of the webhook.\"\n flag \"--state \" help=\"Optional. The state of the webhook. (options: enabled, disabled, disabled_due_to_failed_deliveries)\"\n flag \"--subscribed-events \" help=\"\"\"\n Optional. The events that the webhook is subscribed to.\n Available events:\n - batch.succeeded\n - batch.expired\n - batch.failed\n - interaction.requires_action\n - interaction.completed\n - interaction.failed\n - video.generated\n \"\"\" var=#true\n flag \"--uri \" help=\"Optional. The URI to which webhook events will be sent.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\ncmd \"auth\" help=\"Manage authentication credentials\" {\n cmd \"login\" help=\"Interactively configure authentication credentials\"\n cmd \"logout\" help=\"Clear all stored authentication credentials\"\n cmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n}\ncmd \"completion\" help=\"Generate the autocompletion script for the specified shell\" {\n cmd \"bash\" help=\"Generate the autocompletion script for bash\"\n cmd \"fish\" help=\"Generate the autocompletion script for fish\"\n cmd \"powershell\" help=\"Generate the autocompletion script for powershell\"\n cmd \"zsh\" help=\"Generate the autocompletion script for zsh\"\n}\ncmd \"credentials\" help=\"Operations for credentials\" {\n cmd \"create\" help=\"Creates a credential.\" {\n flag \"--body-param \" help=\"{ \\\"id\\\": string, \\\"injection_location\\\": string | string[], \\\"value\\\": string, ... } | { \\\"id\\\": string, \\\"token\\\": string, ... } | { \\\"client_id\\\": string, \\\"client_secret\\\": string, \\\"id\\\": string, \\\"refresh_token\\\": string, ... }\"\n flag \"--body-param.environment-variable \" help=\"EnvironmentVariableConfig variant as JSON\"\n flag \"--body-param.bearer-token \" help=\"HttpBearerConfig variant as JSON\"\n flag \"--body-param.bearer-token.header-name \" help=\"\"\"\n Optional. Header name to inject the token into. Defaults to\n 'Authorization'.\n \"\"\"\n flag \"--body-param.bearer-token.id \" help=\"[required]\"\n flag \"--body-param.bearer-token.prefix \" help=\"\"\"\n Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\n for no prefix.\n \"\"\"\n flag \"--body-param.bearer-token.token \" help=\"Required. Input only. The static bearer token. Write-only; never returned in responses. [required]\"\n flag \"--body-param.oauth2 \" help=\"OAuth2Config variant as JSON\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Deletes a credential. Fails if referenced by active triggers.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"get\" help=\"Gets metadata of a single credential (no secret fields).\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"list\" help=\"Lists credentials for a project.\" {\n flag \"--page-size \" help=\"\"\"\n Optional. Maximum number of credentials to return.\n If unspecified, defaults to 50. Maximum is 1000.\n \"\"\"\n flag \"--page-token \" help=\"Optional. Pagination token.\"\n }\n cmd \"update\" help=\"Updates a credential.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n flag \"--update-mask \" help=\"Optional. The list of fields to update.\"\n flag \"--body-param \" help=\"{ \\\"injection_location\\\": string | string[], \\\"trusted_domains\\\": string[], \\\"value\\\": string } | { \\\"header_name\\\": string, \\\"prefix\\\": string, \\\"token\\\": string } | { \\\"client_id\\\": string, \\\"client_secret\\\": string, \\\"refresh_token\\\": string, \\\"scopes\\\": string[], ... }\"\n flag \"--body-param.environment-variable \" help=\"EnvironmentVariableUpdateConfig variant as JSON\"\n flag \"--body-param.bearer-token \" help=\"HttpBearerUpdateConfig variant as JSON\"\n flag \"--body-param.bearer-token.header-name \" help=\"\"\"\n Optional. Header name to inject the token into. Defaults to\n 'Authorization'.\n \"\"\"\n flag \"--body-param.bearer-token.prefix \" help=\"\"\"\n Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\n for no prefix.\n \"\"\"\n flag \"--body-param.bearer-token.token \" help=\"Optional. Input only. The static bearer token. Write-only; never returned in responses.\"\n flag \"--body-param.oauth2 \" help=\"OAuth2UpdateConfig variant as JSON\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\ncmd \"environments\" help=\"Operations for environments\" {\n cmd \"create\" help=\"Creates an environment.\" {\n flag \"--from-environment \" help=\"\"\"\n Optional. The source environment to copy/fork from.\n Format: `environments/{environment_id}` or `{environment_id}`.\n When specified, `sources` and `env` must be empty.\n \"\"\"\n flag \"--network \" help=\"{ \\\"allowlist\\\": object[] } | Disabled | CreateEnvironmentRequest_network_enum\"\n flag \"--sources \" help=\"Sources to be mounted into the environment.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Deletes an environment.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"files\" help=\"Operations for files\" {\n cmd \"list\" help=\"Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper.\" {\n flag \"--environment \" help=\"[required]\"\n flag \"--path \" help=\"[required]\"\n flag \"--page-size \" help=\"Optional. Maximum number of entries to return per page (for directory listing).\"\n flag \"--page-token \" help=\"Optional. Pagination token for directory listing.\"\n flag \"--recursive\" help=\"Optional. If true and the path is a directory, recursively lists all files.\"\n }\n }\n cmd \"get\" help=\"Gets an environment.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"internal\" help=\"Operations for internal\" {\n cmd \"start-upload\" help=\"Start an environment file upload\" {\n alias \"su\"\n flag \"--environment \" help=\"The ID of the environment that owns the destination file. [required]\"\n flag \"--path \" help=\"The relative destination path inside the environment workspace. [required]\"\n flag \"--extract\" help=\"Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.\"\n flag \"--overwrite\" help=\"Optional. Whether to overwrite the destination file if it already exists.\"\n flag \"--x-goog-upload-header-content-length \" help=\"Total number of file bytes that will be uploaded to the session URL. [required]\"\n flag \"--x-goog-upload-header-content-type \" help=\"MIME type of the file that will be uploaded to the session URL. [required]\"\n }\n }\n cmd \"list\" help=\"Lists environments.\" {\n flag \"--page-size \" help=\"Optional. Maximum number of environments to return.\\\\nIf unspecified, defaults to 50. Maximum is 1000.\"\n flag \"--page-token \" help=\"Optional. Pagination token.\"\n }\n}\ncmd \"explore\" help=\"Interactively browse and run commands\"\ncmd \"help\" help=\"Help about any command\"\ncmd \"version\" help=\"Print the CLI version\"\ncmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n", + "agent": "cmd \"agent\" help=\"Run interactions with Gemini models or managed agents, and manage agent definitions\" {\n cmd \"run\" help=\"Run an interaction with a Gemini model or a managed agent\" {\n arg \"input\" help=\"Prompt or task to send\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--agent \" help=\"Managed agent to run (see \\\"gemini-api agent list\\\") (e.g. deep-research-pro-preview-12-2025, deep-research-preview-04-2026, deep-research-max-preview-04-2026, antigravity-preview-05-2026)\"\n flag \"--background\" help=\"Return immediately with an interaction ID; poll with \\\"gemini-api agent status\\\"\"\n flag \"-m --model \" help=\"Model to run (see \\\"gemini-api models\\\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--stream\" help=\"Stream the reply as it is generated; use --stream=false for one complete interaction (default: true)\"\n }\n cmd \"cancel\" help=\"Cancel an in-progress interaction by interaction ID\" {\n flag \"--id \" help=\"The unique identifier of the interaction to cancel. [required]\"\n }\n cmd \"create\" help=\"Create a managed agent definition\" {\n flag \"--agent-config \" help=\"{ \\\"max_total_tokens\\\": string, \\\"model\\\": string }\"\n flag \"--base-agent \" help=\"The base agent to extend. [required]\"\n flag \"--base-environment \" help=\"{ \\\"env\\\": object, \\\"environment_id\\\": string, \\\"network\\\": object | string | string, \\\"sources\\\": object[] } | string\"\n flag \"--description \" help=\"Agent description for developers to quickly read and understand.\"\n flag \"--id \" help=\"The unique identifier for the agent. [required]\"\n flag \"--system-instruction \" help=\"System instruction for the agent.\"\n flag \"--tools \" help=\"The tools available to the agent.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Delete a managed agent definition by ID\" {\n flag \"--id \" help=\"[required]\"\n }\n cmd \"delete-interaction\" help=\"Delete an interaction by interaction ID\" {\n alias \"di\"\n flag \"--id \" help=\"The unique identifier of the interaction to delete. [required]\"\n }\n cmd \"get\" help=\"Get a managed agent definition by ID\" {\n flag \"--id \" help=\"[required]\"\n }\n cmd \"list\" help=\"List managed agent definitions\" {\n flag \"--page-size \" help=\"integer value\"\n flag \"--page-token \" help=\"string value\"\n flag \"--parent \" help=\"string value\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"status\" help=\"Get status and output of an interaction by interaction ID\" {\n flag \"--id \" help=\"The unique identifier of the interaction to retrieve. [required]\"\n flag \"--include-input\" help=\"If set to true, includes the input in the response.\" default=#false\n flag \"--last-event-id \" help=\"Optional. If set, resumes the interaction stream from the next chunk after the event marked by the event id. Can only be used if `stream` is true.\"\n flag \"--stream\" help=\"Stream the interaction's events (replayed from the start for a finished interaction) instead of returning the status object. Defaults to true; use --stream=false for the status object.\" default=#true\n }\n}\n", + "agent run": "cmd \"run\" help=\"Run an interaction with a Gemini model or a managed agent\" {\n arg \"input\" help=\"Prompt or task to send\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--agent \" help=\"Managed agent to run (see \\\"gemini-api agent list\\\") (e.g. deep-research-pro-preview-12-2025, deep-research-preview-04-2026, deep-research-max-preview-04-2026, antigravity-preview-05-2026)\"\n flag \"--background\" help=\"Return immediately with an interaction ID; poll with \\\"gemini-api agent status\\\"\"\n flag \"-m --model \" help=\"Model to run (see \\\"gemini-api models\\\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--stream\" help=\"Stream the reply as it is generated; use --stream=false for one complete interaction (default: true)\"\n}\n", + "agent cancel": "cmd \"cancel\" help=\"Cancel an in-progress interaction by interaction ID\" {\n flag \"--id \" help=\"The unique identifier of the interaction to cancel. [required]\"\n}\n", + "agent create": "cmd \"create\" help=\"Create a managed agent definition\" {\n flag \"--agent-config \" help=\"{ \\\"max_total_tokens\\\": string, \\\"model\\\": string }\"\n flag \"--base-agent \" help=\"The base agent to extend. [required]\"\n flag \"--base-environment \" help=\"{ \\\"env\\\": object, \\\"environment_id\\\": string, \\\"network\\\": object | string | string, \\\"sources\\\": object[] } | string\"\n flag \"--description \" help=\"Agent description for developers to quickly read and understand.\"\n flag \"--id \" help=\"The unique identifier for the agent. [required]\"\n flag \"--system-instruction \" help=\"System instruction for the agent.\"\n flag \"--tools \" help=\"The tools available to the agent.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "agent delete": "cmd \"delete\" help=\"Delete a managed agent definition by ID\" {\n flag \"--id \" help=\"[required]\"\n}\n", + "agent delete-interaction": "cmd \"delete-interaction\" help=\"Delete an interaction by interaction ID\" {\n alias \"di\"\n flag \"--id \" help=\"The unique identifier of the interaction to delete. [required]\"\n}\n", + "agent di": "cmd \"delete-interaction\" help=\"Delete an interaction by interaction ID\" {\n alias \"di\"\n flag \"--id \" help=\"The unique identifier of the interaction to delete. [required]\"\n}\n", + "agent get": "cmd \"get\" help=\"Get a managed agent definition by ID\" {\n flag \"--id \" help=\"[required]\"\n}\n", + "agent list": "cmd \"list\" help=\"List managed agent definitions\" {\n flag \"--page-size \" help=\"integer value\"\n flag \"--page-token \" help=\"string value\"\n flag \"--parent \" help=\"string value\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "agent status": "cmd \"status\" help=\"Get status and output of an interaction by interaction ID\" {\n flag \"--id \" help=\"The unique identifier of the interaction to retrieve. [required]\"\n flag \"--include-input\" help=\"If set to true, includes the input in the response.\" default=#false\n flag \"--last-event-id \" help=\"Optional. If set, resumes the interaction stream from the next chunk after the event marked by the event id. Can only be used if `stream` is true.\"\n flag \"--stream\" help=\"Stream the interaction's events (replayed from the start for a finished interaction) instead of returning the status object. Defaults to true; use --stream=false for the status object.\" default=#true\n}\n", + "analyze": "cmd \"analyze\" help=\"Ask questions about video, audio, PDF, or image files\"\n", + "batch": "cmd \"batch\" help=\"Async batch jobs at reduced cost\"\n", + "configure": "cmd \"configure\" help=\"Configure authentication, global parameters, and preferences\"\n", + "docs": "cmd \"docs\" help=\"Gemini API documentation & guides\"\n", + "embed": "cmd \"embed\" help=\"Vector embeddings (gemini-embedding-2)\"\n", + "files": "cmd \"files\" help=\"Upload / list / download / delete media (48h TTL)\" {\n cmd \"delete\" help=\"Deletes the `File`.\" {\n flag \"--file \" help=\"[required]\"\n }\n cmd \"get\" help=\"Gets the metadata for the given `File`.\" {\n flag \"--file \" help=\"[required]\"\n }\n cmd \"list\" help=\"Lists the metadata for `File`s owned by the requesting project.\" {\n flag \"--page-size \" help=\"Optional. Maximum number of `File`s to return per page. If unspecified, defaults to 10. Maximum `page_size` is 100.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous `ListFiles` call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"register\" help=\"Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails.\" {\n flag \"--uris \" help=\"Required. The Google Cloud Storage URIs to register. Example: `gs://bucket/object`.\" var=#true\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\n", + "files delete": "cmd \"delete\" help=\"Deletes the `File`.\" {\n flag \"--file \" help=\"[required]\"\n}\n", + "files get": "cmd \"get\" help=\"Gets the metadata for the given `File`.\" {\n flag \"--file \" help=\"[required]\"\n}\n", + "files list": "cmd \"list\" help=\"Lists the metadata for `File`s owned by the requesting project.\" {\n flag \"--page-size \" help=\"Optional. Maximum number of `File`s to return per page. If unspecified, defaults to 10. Maximum `page_size` is 100.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous `ListFiles` call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "files register": "cmd \"register\" help=\"Registers a Google Cloud Storage files with FileService. The user is expected to provide Google Cloud Storage URIs and will receive a File resource for each URI in return. Note that the files are not copied, just registered with File API. If one file fails to register, the whole request fails.\" {\n flag \"--uris \" help=\"Required. The Google Cloud Storage URIs to register. Example: `gs://bucket/object`.\" var=#true\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "generate": "cmd \"generate\" help=\"Text & multimodal generation (gemini-3.6-flash)\" {\n arg \"prompt\" help=\"Prompt to send to the model\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Model to use (see \\\"gemini-api models\\\") (default: gemini-3.6-flash) (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--stream\" help=\"Stream the reply as it is generated; use --stream=false for a single complete result (default: true)\"\n}\n", + "image": "cmd \"image\" help=\"Generate or edit images (gemini-3.1-flash-image)\" {\n arg \"prompt\" help=\"Image prompt\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Override the image model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./gemini-image-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n}\n", + "models": "cmd \"models\" help=\"List available models and the default\" {\n cmd \"get\" help=\"Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information.\" {\n flag \"--model \" help=\"[required]\"\n }\n cmd \"list\" help=\"Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API.\" {\n flag \"--page-size \" help=\"The maximum number of `Models` to return (per page). If unspecified, 50 models will be returned per page. This method returns at most 1000 models per page, even if you pass a larger page_size.\"\n flag \"--page-token \" help=\"A page token, received from a previous `ListModels` call. Provide the `page_token` returned by one request as an argument to the next request to retrieve the next page. When paginating, all other parameters provided to `ListModels` must match the call that provided the page token.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n}\n", + "models get": "cmd \"get\" help=\"Gets information about a specific `Model` such as its version number, token limits, [parameters](https://ai.google.dev/gemini-api/docs/models/generative-models#model-parameters) and other metadata. Refer to the [Gemini models guide](https://ai.google.dev/gemini-api/docs/models/gemini) for detailed model information.\" {\n flag \"--model \" help=\"[required]\"\n}\n", + "models list": "cmd \"list\" help=\"Lists the [`Model`s](https://ai.google.dev/gemini-api/docs/models/gemini) available through the Gemini API.\" {\n flag \"--page-size \" help=\"The maximum number of `Models` to return (per page). If unspecified, 50 models will be returned per page. This method returns at most 1000 models per page, even if you pass a larger page_size.\"\n flag \"--page-token \" help=\"A page token, received from a previous `ListModels` call. Provide the `page_token` returned by one request as an argument to the next request to retrieve the next page. When paginating, all other parameters provided to `ListModels` must match the call that provided the page token.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "music": "cmd \"music\" help=\"Music generation (lyria-3-pro-preview)\" {\n arg \"prompt\" help=\"Music prompt\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Override the music model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--out \" help=\"Write the audio to this file (or into this directory). Default: ./gemini-music-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the audio to a file\"\n}\n", + "tokens": "cmd \"tokens\" help=\"Count tokens without generating\"\n", + "transcribe": "cmd \"transcribe\" help=\"Audio/video → text (timestamps, captions)\"\n", + "triggers": "cmd \"triggers\" help=\"Schedule and manage cron triggers that run managed agents\" {\n cmd \"delete\" help=\"Delete a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n }\n cmd \"get\" help=\"Get a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n }\n cmd \"list\" help=\"List triggers for a project\" {\n flag \"--filter \" help=\"Optional. Filter expression (e.g., by state).\"\n flag \"--page-size \" help=\"Optional. The maximum number of triggers to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggers call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"list-executions\" help=\"List executions for a trigger\" {\n alias \"le\"\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n flag \"--page-size \" help=\"Optional. The maximum number of executions to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggerExecutions call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"run\" help=\"Run a trigger immediately\" {\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n }\n cmd \"update\" help=\"Update a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n flag \"--display-name \" help=\"Optional. The display name of the trigger.\"\n flag \"--status \" help=\"Optional. The status of the trigger. (options: active, paused, error)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\n", + "triggers delete": "cmd \"delete\" help=\"Delete a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n}\n", + "triggers get": "cmd \"get\" help=\"Get a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n}\n", + "triggers list": "cmd \"list\" help=\"List triggers for a project\" {\n flag \"--filter \" help=\"Optional. Filter expression (e.g., by state).\"\n flag \"--page-size \" help=\"Optional. The maximum number of triggers to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggers call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "triggers list-executions": "cmd \"list-executions\" help=\"List executions for a trigger\" {\n alias \"le\"\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n flag \"--page-size \" help=\"Optional. The maximum number of executions to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggerExecutions call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "triggers le": "cmd \"list-executions\" help=\"List executions for a trigger\" {\n alias \"le\"\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n flag \"--page-size \" help=\"Optional. The maximum number of executions to return per page.\"\n flag \"--page-token \" help=\"Optional. A page token from a previous ListTriggerExecutions call.\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "triggers run": "cmd \"run\" help=\"Run a trigger immediately\" {\n flag \"--trigger-id \" help=\"Resource name of the trigger. [required]\"\n}\n", + "triggers update": "cmd \"update\" help=\"Update a trigger by ID\" {\n flag \"--id \" help=\"Resource name of the trigger. [required]\"\n flag \"--display-name \" help=\"Optional. The display name of the trigger.\"\n flag \"--status \" help=\"Optional. The status of the trigger. (options: active, paused, error)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "tts": "cmd \"tts\" help=\"Text to speech (gemini-3.1-flash-tts-preview)\"\n", + "video": "cmd \"video\" help=\"Generate & edit video conversationally (gemini-omni-flash-preview)\" {\n arg \"prompt\" help=\"Video prompt\" required=#true var=#true\n flag \"--body-param \" help=\"{ \\\"agent\\\": string, \\\"input\\\": object | object[] | string, \\\"stream\\\": boolean (default: true), ... } | { \\\"input\\\": object | object[] | string, \\\"model\\\": string (default: gemini-3.6-flash), \\\"stream\\\": boolean (default: true), ... }\"\n flag \"--body \" help=\"Request body as JSON (advanced; replaces intent arguments). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"-m --model \" help=\"Override the video model (e.g. gemini-2.5-flash, gemini-2.5-pro, gemma-4-26b-a4b-it, gemma-4-31b-it, ...)\"\n flag \"--out \" help=\"Write the video to this file (or into this directory). Default: ./gemini-video-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the video to a file\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\n", + "webhooks": "cmd \"webhooks\" help=\"Manage webhook endpoints and signing secrets for event delivery\" {\n cmd \"create\" help=\"Create a webhook endpoint\" {\n flag \"--name \" help=\"Optional. The user-provided name of the webhook.\"\n flag \"--subscribed-events \" help=\"\"\"\n Required. The events that the webhook is subscribed to.\n Available events:\n - batch.succeeded\n - batch.expired\n - batch.failed\n - interaction.requires_action\n - interaction.completed\n - interaction.failed\n - video.generated [required]\n \"\"\" var=#true\n flag \"--uri \" help=\"Required. The URI to which webhook events will be sent. [required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Delete a webhook by ID\" {\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook to delete.\n Format: `{webhook_id}` [required]\n \"\"\"\n }\n cmd \"get\" help=\"Get a webhook by ID\" {\n flag \"--id \" help=\"Required. The ID of the webhook to retrieve. [required]\"\n }\n cmd \"list\" help=\"List webhook endpoints\" {\n flag \"--page-size \" help=\"\"\"\n Optional. The maximum number of webhooks to return. The service may return fewer than\n this value. If unspecified, at most 50 webhooks will be returned.\n The maximum value is 1000.\n \"\"\"\n flag \"--page-token \" help=\"\"\"\n Optional. A page token, received from a previous `ListWebhooks` call.\n Provide this to retrieve the subsequent page.\n \"\"\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"ping\" help=\"Send a ping event to a webhook\" {\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook to ping.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--body-param \" help=\"The request body.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"rotate-signing-secret\" help=\"Rotate the signing secret for a webhook\" {\n alias \"rss\"\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook for which to generate a signing secret.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--revocation-behavior \" help=\"Optional. The revocation behavior for previous signing secrets. (options: revoke_previous_secrets_after_h24, revoke_previous_secrets_immediately)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"update\" help=\"Update a webhook by ID\" {\n flag \"--id \" help=\"Required. The ID of the webhook to update. [required]\"\n flag \"--update-mask \" help=\"Optional. The list of fields to update.\"\n flag \"--name \" help=\"Optional. The user-provided name of the webhook.\"\n flag \"--state \" help=\"Optional. The state of the webhook. (options: enabled, disabled, disabled_due_to_failed_deliveries)\"\n flag \"--subscribed-events \" help=\"\"\"\n Optional. The events that the webhook is subscribed to.\n Available events:\n - batch.succeeded\n - batch.expired\n - batch.failed\n - interaction.requires_action\n - interaction.completed\n - interaction.failed\n - video.generated\n \"\"\" var=#true\n flag \"--uri \" help=\"Optional. The URI to which webhook events will be sent.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\n", + "webhooks create": "cmd \"create\" help=\"Create a webhook endpoint\" {\n flag \"--name \" help=\"Optional. The user-provided name of the webhook.\"\n flag \"--subscribed-events \" help=\"\"\"\n Required. The events that the webhook is subscribed to.\n Available events:\n - batch.succeeded\n - batch.expired\n - batch.failed\n - interaction.requires_action\n - interaction.completed\n - interaction.failed\n - video.generated [required]\n \"\"\" var=#true\n flag \"--uri \" help=\"Required. The URI to which webhook events will be sent. [required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "webhooks delete": "cmd \"delete\" help=\"Delete a webhook by ID\" {\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook to delete.\n Format: `{webhook_id}` [required]\n \"\"\"\n}\n", + "webhooks get": "cmd \"get\" help=\"Get a webhook by ID\" {\n flag \"--id \" help=\"Required. The ID of the webhook to retrieve. [required]\"\n}\n", + "webhooks list": "cmd \"list\" help=\"List webhook endpoints\" {\n flag \"--page-size \" help=\"\"\"\n Optional. The maximum number of webhooks to return. The service may return fewer than\n this value. If unspecified, at most 50 webhooks will be returned.\n The maximum value is 1000.\n \"\"\"\n flag \"--page-token \" help=\"\"\"\n Optional. A page token, received from a previous `ListWebhooks` call.\n Provide this to retrieve the subsequent page.\n \"\"\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\n", + "webhooks ping": "cmd \"ping\" help=\"Send a ping event to a webhook\" {\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook to ping.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--body-param \" help=\"The request body.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "webhooks rotate-signing-secret": "cmd \"rotate-signing-secret\" help=\"Rotate the signing secret for a webhook\" {\n alias \"rss\"\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook for which to generate a signing secret.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--revocation-behavior \" help=\"Optional. The revocation behavior for previous signing secrets. (options: revoke_previous_secrets_after_h24, revoke_previous_secrets_immediately)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "webhooks rss": "cmd \"rotate-signing-secret\" help=\"Rotate the signing secret for a webhook\" {\n alias \"rss\"\n flag \"--id \" help=\"\"\"\n Required. The ID of the webhook for which to generate a signing secret.\n Format: `{webhook_id}` [required]\n \"\"\"\n flag \"--revocation-behavior \" help=\"Optional. The revocation behavior for previous signing secrets. (options: revoke_previous_secrets_after_h24, revoke_previous_secrets_immediately)\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "webhooks update": "cmd \"update\" help=\"Update a webhook by ID\" {\n flag \"--id \" help=\"Required. The ID of the webhook to update. [required]\"\n flag \"--update-mask \" help=\"Optional. The list of fields to update.\"\n flag \"--name \" help=\"Optional. The user-provided name of the webhook.\"\n flag \"--state \" help=\"Optional. The state of the webhook. (options: enabled, disabled, disabled_due_to_failed_deliveries)\"\n flag \"--subscribed-events \" help=\"\"\"\n Optional. The events that the webhook is subscribed to.\n Available events:\n - batch.succeeded\n - batch.expired\n - batch.failed\n - interaction.requires_action\n - interaction.completed\n - interaction.failed\n - video.generated\n \"\"\" var=#true\n flag \"--uri \" help=\"Optional. The URI to which webhook events will be sent.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "auth": "cmd \"auth\" help=\"Manage authentication credentials\" {\n cmd \"login\" help=\"Interactively configure authentication credentials\"\n cmd \"logout\" help=\"Clear all stored authentication credentials\"\n cmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n}\n", + "auth login": "cmd \"login\" help=\"Interactively configure authentication credentials\"\n", + "auth logout": "cmd \"logout\" help=\"Clear all stored authentication credentials\"\n", + "auth whoami": "cmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n", + "completion": "cmd \"completion\" help=\"Generate the autocompletion script for the specified shell\" {\n cmd \"bash\" help=\"Generate the autocompletion script for bash\"\n cmd \"fish\" help=\"Generate the autocompletion script for fish\"\n cmd \"powershell\" help=\"Generate the autocompletion script for powershell\"\n cmd \"zsh\" help=\"Generate the autocompletion script for zsh\"\n}\n", + "completion bash": "cmd \"bash\" help=\"Generate the autocompletion script for bash\"\n", + "completion fish": "cmd \"fish\" help=\"Generate the autocompletion script for fish\"\n", + "completion powershell": "cmd \"powershell\" help=\"Generate the autocompletion script for powershell\"\n", + "completion zsh": "cmd \"zsh\" help=\"Generate the autocompletion script for zsh\"\n", + "credentials": "cmd \"credentials\" help=\"Operations for credentials\" {\n cmd \"create\" help=\"Creates a credential.\" {\n flag \"--body-param \" help=\"{ \\\"id\\\": string, \\\"injection_location\\\": string | string[], \\\"value\\\": string, ... } | { \\\"id\\\": string, \\\"token\\\": string, ... } | { \\\"client_id\\\": string, \\\"client_secret\\\": string, \\\"id\\\": string, \\\"refresh_token\\\": string, ... }\"\n flag \"--body-param.environment-variable \" help=\"EnvironmentVariableConfig variant as JSON\"\n flag \"--body-param.bearer-token \" help=\"HttpBearerConfig variant as JSON\"\n flag \"--body-param.bearer-token.header-name \" help=\"\"\"\n Optional. Header name to inject the token into. Defaults to\n 'Authorization'.\n \"\"\"\n flag \"--body-param.bearer-token.id \" help=\"[required]\"\n flag \"--body-param.bearer-token.prefix \" help=\"\"\"\n Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\n for no prefix.\n \"\"\"\n flag \"--body-param.bearer-token.token \" help=\"Required. Input only. The static bearer token. Write-only; never returned in responses. [required]\"\n flag \"--body-param.oauth2 \" help=\"OAuth2Config variant as JSON\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Deletes a credential. Fails if referenced by active triggers.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"get\" help=\"Gets metadata of a single credential (no secret fields).\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"list\" help=\"Lists credentials for a project.\" {\n flag \"--page-size \" help=\"\"\"\n Optional. Maximum number of credentials to return.\n If unspecified, defaults to 50. Maximum is 1000.\n \"\"\"\n flag \"--page-token \" help=\"Optional. Pagination token.\"\n }\n cmd \"update\" help=\"Updates a credential.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n flag \"--update-mask \" help=\"Optional. The list of fields to update.\"\n flag \"--body-param \" help=\"{ \\\"injection_location\\\": string | string[], \\\"trusted_domains\\\": string[], \\\"value\\\": string } | { \\\"header_name\\\": string, \\\"prefix\\\": string, \\\"token\\\": string } | { \\\"client_id\\\": string, \\\"client_secret\\\": string, \\\"refresh_token\\\": string, \\\"scopes\\\": string[], ... }\"\n flag \"--body-param.environment-variable \" help=\"EnvironmentVariableUpdateConfig variant as JSON\"\n flag \"--body-param.bearer-token \" help=\"HttpBearerUpdateConfig variant as JSON\"\n flag \"--body-param.bearer-token.header-name \" help=\"\"\"\n Optional. Header name to inject the token into. Defaults to\n 'Authorization'.\n \"\"\"\n flag \"--body-param.bearer-token.prefix \" help=\"\"\"\n Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\n for no prefix.\n \"\"\"\n flag \"--body-param.bearer-token.token \" help=\"Optional. Input only. The static bearer token. Write-only; never returned in responses.\"\n flag \"--body-param.oauth2 \" help=\"OAuth2UpdateConfig variant as JSON\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n}\n", + "credentials create": "cmd \"create\" help=\"Creates a credential.\" {\n flag \"--body-param \" help=\"{ \\\"id\\\": string, \\\"injection_location\\\": string | string[], \\\"value\\\": string, ... } | { \\\"id\\\": string, \\\"token\\\": string, ... } | { \\\"client_id\\\": string, \\\"client_secret\\\": string, \\\"id\\\": string, \\\"refresh_token\\\": string, ... }\"\n flag \"--body-param.environment-variable \" help=\"EnvironmentVariableConfig variant as JSON\"\n flag \"--body-param.bearer-token \" help=\"HttpBearerConfig variant as JSON\"\n flag \"--body-param.bearer-token.header-name \" help=\"\"\"\n Optional. Header name to inject the token into. Defaults to\n 'Authorization'.\n \"\"\"\n flag \"--body-param.bearer-token.id \" help=\"[required]\"\n flag \"--body-param.bearer-token.prefix \" help=\"\"\"\n Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\n for no prefix.\n \"\"\"\n flag \"--body-param.bearer-token.token \" help=\"Required. Input only. The static bearer token. Write-only; never returned in responses. [required]\"\n flag \"--body-param.oauth2 \" help=\"OAuth2Config variant as JSON\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "credentials delete": "cmd \"delete\" help=\"Deletes a credential. Fails if referenced by active triggers.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n}\n", + "credentials get": "cmd \"get\" help=\"Gets metadata of a single credential (no secret fields).\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n}\n", + "credentials list": "cmd \"list\" help=\"Lists credentials for a project.\" {\n flag \"--page-size \" help=\"\"\"\n Optional. Maximum number of credentials to return.\n If unspecified, defaults to 50. Maximum is 1000.\n \"\"\"\n flag \"--page-token \" help=\"Optional. Pagination token.\"\n}\n", + "credentials update": "cmd \"update\" help=\"Updates a credential.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n flag \"--update-mask \" help=\"Optional. The list of fields to update.\"\n flag \"--body-param \" help=\"{ \\\"injection_location\\\": string | string[], \\\"trusted_domains\\\": string[], \\\"value\\\": string } | { \\\"header_name\\\": string, \\\"prefix\\\": string, \\\"token\\\": string } | { \\\"client_id\\\": string, \\\"client_secret\\\": string, \\\"refresh_token\\\": string, \\\"scopes\\\": string[], ... }\"\n flag \"--body-param.environment-variable \" help=\"EnvironmentVariableUpdateConfig variant as JSON\"\n flag \"--body-param.bearer-token \" help=\"HttpBearerUpdateConfig variant as JSON\"\n flag \"--body-param.bearer-token.header-name \" help=\"\"\"\n Optional. Header name to inject the token into. Defaults to\n 'Authorization'.\n \"\"\"\n flag \"--body-param.bearer-token.prefix \" help=\"\"\"\n Optional. Prefix to prepend to the token. Defaults to 'Bearer'. Set to ''\n for no prefix.\n \"\"\"\n flag \"--body-param.bearer-token.token \" help=\"Optional. Input only. The static bearer token. Write-only; never returned in responses.\"\n flag \"--body-param.oauth2 \" help=\"OAuth2UpdateConfig variant as JSON\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "environments": "cmd \"environments\" help=\"Operations for environments\" {\n cmd \"create\" help=\"Creates an environment.\" {\n flag \"--from-environment \" help=\"\"\"\n Optional. The source environment to copy/fork from.\n Format: `environments/{environment_id}` or `{environment_id}`.\n When specified, `sources` and `env` must be empty.\n \"\"\"\n flag \"--network \" help=\"{ \\\"allowlist\\\": object[] } | Disabled | CreateEnvironmentRequest_network_enum\"\n flag \"--sources \" help=\"Sources to be mounted into the environment.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"delete\" help=\"Deletes an environment.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"files\" help=\"Operations for files\" {\n cmd \"list\" help=\"Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper.\" {\n flag \"--environment \" help=\"[required]\"\n flag \"--path \" help=\"[required]\"\n flag \"--page-size \" help=\"Optional. Maximum number of entries to return per page (for directory listing).\"\n flag \"--page-token \" help=\"Optional. Pagination token for directory listing.\"\n flag \"--recursive\" help=\"Optional. If true and the path is a directory, recursively lists all files.\"\n }\n }\n cmd \"get\" help=\"Gets an environment.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n }\n cmd \"internal\" help=\"Operations for internal\" {\n cmd \"start-upload\" help=\"Start an environment file upload\" {\n alias \"su\"\n flag \"--environment \" help=\"The ID of the environment that owns the destination file. [required]\"\n flag \"--path \" help=\"The relative destination path inside the environment workspace. [required]\"\n flag \"--extract\" help=\"Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.\"\n flag \"--overwrite\" help=\"Optional. Whether to overwrite the destination file if it already exists.\"\n flag \"--x-goog-upload-header-content-length \" help=\"Total number of file bytes that will be uploaded to the session URL. [required]\"\n flag \"--x-goog-upload-header-content-type \" help=\"MIME type of the file that will be uploaded to the session URL. [required]\"\n }\n }\n cmd \"list\" help=\"Lists environments.\" {\n flag \"--page-size \" help=\"Optional. Maximum number of environments to return.\\\\nIf unspecified, defaults to 50. Maximum is 1000.\"\n flag \"--page-token \" help=\"Optional. Pagination token.\"\n }\n}\n", + "environments create": "cmd \"create\" help=\"Creates an environment.\" {\n flag \"--from-environment \" help=\"\"\"\n Optional. The source environment to copy/fork from.\n Format: `environments/{environment_id}` or `{environment_id}`.\n When specified, `sources` and `env` must be empty.\n \"\"\"\n flag \"--network \" help=\"{ \\\"allowlist\\\": object[] } | Disabled | CreateEnvironmentRequest_network_enum\"\n flag \"--sources \" help=\"Sources to be mounted into the environment.\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\n", + "environments delete": "cmd \"delete\" help=\"Deletes an environment.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n}\n", + "environments files": "cmd \"files\" help=\"Operations for files\" {\n cmd \"list\" help=\"Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper.\" {\n flag \"--environment \" help=\"[required]\"\n flag \"--path \" help=\"[required]\"\n flag \"--page-size \" help=\"Optional. Maximum number of entries to return per page (for directory listing).\"\n flag \"--page-token \" help=\"Optional. Pagination token for directory listing.\"\n flag \"--recursive\" help=\"Optional. If true and the path is a directory, recursively lists all files.\"\n }\n}\n", + "environments files list": "cmd \"list\" help=\"Retrieves file metadata or directory contents from an environment's snapshot. To download file contents directly, pass ?alt=media or use the files.download helper.\" {\n flag \"--environment \" help=\"[required]\"\n flag \"--path \" help=\"[required]\"\n flag \"--page-size \" help=\"Optional. Maximum number of entries to return per page (for directory listing).\"\n flag \"--page-token \" help=\"Optional. Pagination token for directory listing.\"\n flag \"--recursive\" help=\"Optional. If true and the path is a directory, recursively lists all files.\"\n}\n", + "environments get": "cmd \"get\" help=\"Gets an environment.\" {\n flag \"--id \" help=\"Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. [required]\"\n}\n", + "environments internal": "cmd \"internal\" help=\"Operations for internal\" {\n cmd \"start-upload\" help=\"Start an environment file upload\" {\n alias \"su\"\n flag \"--environment \" help=\"The ID of the environment that owns the destination file. [required]\"\n flag \"--path \" help=\"The relative destination path inside the environment workspace. [required]\"\n flag \"--extract\" help=\"Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.\"\n flag \"--overwrite\" help=\"Optional. Whether to overwrite the destination file if it already exists.\"\n flag \"--x-goog-upload-header-content-length \" help=\"Total number of file bytes that will be uploaded to the session URL. [required]\"\n flag \"--x-goog-upload-header-content-type \" help=\"MIME type of the file that will be uploaded to the session URL. [required]\"\n }\n}\n", + "environments internal start-upload": "cmd \"start-upload\" help=\"Start an environment file upload\" {\n alias \"su\"\n flag \"--environment \" help=\"The ID of the environment that owns the destination file. [required]\"\n flag \"--path \" help=\"The relative destination path inside the environment workspace. [required]\"\n flag \"--extract\" help=\"Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.\"\n flag \"--overwrite\" help=\"Optional. Whether to overwrite the destination file if it already exists.\"\n flag \"--x-goog-upload-header-content-length \" help=\"Total number of file bytes that will be uploaded to the session URL. [required]\"\n flag \"--x-goog-upload-header-content-type \" help=\"MIME type of the file that will be uploaded to the session URL. [required]\"\n}\n", + "environments internal su": "cmd \"start-upload\" help=\"Start an environment file upload\" {\n alias \"su\"\n flag \"--environment \" help=\"The ID of the environment that owns the destination file. [required]\"\n flag \"--path \" help=\"The relative destination path inside the environment workspace. [required]\"\n flag \"--extract\" help=\"Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.\"\n flag \"--overwrite\" help=\"Optional. Whether to overwrite the destination file if it already exists.\"\n flag \"--x-goog-upload-header-content-length \" help=\"Total number of file bytes that will be uploaded to the session URL. [required]\"\n flag \"--x-goog-upload-header-content-type \" help=\"MIME type of the file that will be uploaded to the session URL. [required]\"\n}\n", + "environments list": "cmd \"list\" help=\"Lists environments.\" {\n flag \"--page-size \" help=\"Optional. Maximum number of environments to return.\\\\nIf unspecified, defaults to 50. Maximum is 1000.\"\n flag \"--page-token \" help=\"Optional. Pagination token.\"\n}\n", + "explore": "cmd \"explore\" help=\"Interactively browse and run commands\"\n", + "help": "cmd \"help\" help=\"Help about any command\"\n", + "version": "cmd \"version\" help=\"Print the CLI version\"\n", + "whoami": "cmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n", +} + +func UsageRequested(cmd *cobra.Command) bool { + if cmd == nil { + return false + } + if flag := cmd.Flags().Lookup("usage"); flag != nil { + value, err := cmd.Flags().GetBool("usage") + return err == nil && value + } + if flag := cmd.InheritedFlags().Lookup("usage"); flag != nil { + value, err := cmd.InheritedFlags().GetBool("usage") + return err == nil && value + } + return false +} + +// DisarmRequiredFlags clears cobra's required-flag annotations so a --usage +// invocation answers with the command's schema even when required inputs are +// absent: requiredness is part of the answer, not a precondition for asking. +// Cobra validates the same annotations itself after the pre-run hooks, so +// skipping the CLI's own eager validation alone would not be enough. The +// cleared annotations are kept aside for RearmRequiredFlags, so a reused +// command tree keeps enforcing requiredness on later executions. +func DisarmRequiredFlags(cmd *cobra.Command) { + saved := map[*pflag.Flag][]string{} + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if values, ok := f.Annotations[cobra.BashCompOneRequiredFlag]; ok { + saved[f] = values + delete(f.Annotations, cobra.BashCompOneRequiredFlag) + } + }) + if len(saved) > 0 { + disarmedRequiredFlags.Store(cmd, saved) + } +} + +// RearmRequiredFlags restores the annotations DisarmRequiredFlags removed. +func RearmRequiredFlags(cmd *cobra.Command) { + value, ok := disarmedRequiredFlags.LoadAndDelete(cmd) + if !ok { + return + } + for f, values := range value.(map[*pflag.Flag][]string) { + if f.Annotations == nil { + f.Annotations = map[string][]string{} + } + f.Annotations[cobra.BashCompOneRequiredFlag] = values + } +} + +var disarmedRequiredFlags sync.Map + +func selectedCommandPath(cmd *cobra.Command) []string { + if cmd == nil { + return nil + } + parts := []string{} + for cur := cmd; cur != nil; cur = cur.Parent() { + if cur.Name() == "" { + continue + } + if cur.Parent() == nil { + continue + } + parts = append([]string{cur.Name()}, parts...) + } + return parts +} + +func EmitSchema(cmd *cobra.Command, w io.Writer) error { + path := strings.Join(selectedCommandPath(cmd), " ") + if cmd.Annotations["speakeasy_usage_dynamic"] == "true" { + return emitLiveSchema(cmd, w) + } + if schema, ok := usageSchemas[path]; ok { + _, err := io.WriteString(w, schema) + return err + } + return emitLiveSchema(cmd, w) +} + +// MarkDynamic makes --usage render the command's schema live from its cobra definition. +func MarkDynamic(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + cmd.Annotations["speakeasy_usage_dynamic"] = "true" +} + +// Go's %q emits \a, \v, and \xNN escapes that KDL v2 does not define. +func kdlQuote(s string) string { + var b strings.Builder + b.WriteByte('"') + for _, r := range s { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 || r == 0x7f || r == 0x85 || + r == 0x200e || r == 0x200f || r == 0x2028 || r == 0x2029 || + (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) || + r == 0xfeff { + fmt.Fprintf(&b, `\u{%x}`, r) + } else { + b.WriteRune(r) + } + } + } + b.WriteByte('"') + return b.String() +} + +// Two consecutive quotes are legal inside a KDL """ string; a third would terminate it. +func kdlEscapeMultilineLine(line string) string { + var b strings.Builder + quoteRun := 0 + for _, r := range line { + if r == '"' { + quoteRun++ + if quoteRun == 3 { + b.WriteString(`\"`) + quoteRun = 0 + continue + } + b.WriteByte('"') + continue + } + quoteRun = 0 + switch r { + case '\\': + b.WriteString(`\\`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 || r == 0x7f || r == 0x85 || + r == 0x200e || r == 0x200f || r == 0x2028 || r == 0x2029 || + (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) || + r == 0xfeff { + fmt.Fprintf(&b, `\u{%x}`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() +} + +// KDL dedent strips the closing line's indent from every line and empties whitespace-only lines. +func kdlQuoteAt(s, contentPrefix string) string { + if !strings.Contains(s, "\n") { + return kdlQuote(s) + } + lines := strings.Split(s, "\n") + for _, line := range lines { + if line != "" && (strings.TrimSpace(line) == "" || strings.TrimRight(line, " \t") != line) { + return kdlQuote(s) + } + } + var b strings.Builder + b.WriteString("\"\"\"\n") + for _, line := range lines { + if line != "" { + b.WriteString(contentPrefix) + b.WriteString(kdlEscapeMultilineLine(line)) + } + b.WriteByte('\n') + } + b.WriteString(contentPrefix) + b.WriteString(`"""`) + return b.String() +} + +func emitLiveSchema(cmd *cobra.Command, w io.Writer) error { + nl := string(rune(10)) + var b strings.Builder + fmt.Fprintf(&b, "cmd %s help=%s {%s", kdlQuote(cmd.Name()), kdlQuoteAt(cmd.Short, " "), nl) + for _, alias := range cmd.Aliases { + fmt.Fprintf(&b, " alias %s%s", kdlQuote(alias), nl) + } + var promptArgs struct { + Version int `json:"version"` + Args []struct { + Name string `json:"name"` + Summary string `json:"summary"` + Required bool `json:"required"` + Variadic bool `json:"variadic"` + } `json:"args"` + } + if raw := cmd.Annotations["speakeasy_prompt_args"]; raw != "" && json.Unmarshal([]byte(raw), &promptArgs) == nil && promptArgs.Version == 1 { + for _, arg := range promptArgs.Args { + fmt.Fprintf(&b, " arg %s", kdlQuote(arg.Name)) + if arg.Summary != "" { + fmt.Fprintf(&b, " help=%s", kdlQuoteAt(arg.Summary, " ")) + } + if arg.Required { + b.WriteString(" required=#true") + } + if arg.Variadic { + b.WriteString(" var=#true") + } + b.WriteString(nl) + } + } else if args := cmd.Annotations["speakeasy_args_help"]; args != "" { + fmt.Fprintf(&b, " args %s%s", kdlQuoteAt(args, " "), nl) + } + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if f.Hidden || f.Name == "help" { + return + } + name := "--" + f.Name + if f.Shorthand != "" { + name = "-" + f.Shorthand + " " + name + } + _, repeatable := f.Value.(pflag.SliceValue) + if f.Value.Type() != "bool" { + placeholder := strings.ReplaceAll(f.Name, "-", "_") + if repeatable { + placeholder += "..." + } + name += " <" + placeholder + ">" + } + fmt.Fprintf(&b, " flag %s help=%s", kdlQuote(name), kdlQuoteAt(f.Usage, " ")) + if repeatable { + b.WriteString(" var=#true") + } + b.WriteString(nl) + }) + b.WriteString("}" + nl) + _, err := io.WriteString(w, b.String()) + return err +} + +func Intercept(root *cobra.Command) { + for _, child := range root.Commands() { + Intercept(child) + } + if root.RunE == nil && root.Run == nil && root.Args == nil && root.HasSubCommands() { + if root.Annotations == nil { + root.Annotations = make(map[string]string) + } + root.Annotations[usageSynthesizedGroupAnnotation] = "true" + } + root.PersistentPreRunE = skipOnUsageE(root.PersistentPreRunE, root.PersistentPreRun) + root.PersistentPreRun = nil + root.PreRunE = skipOnUsageE(root.PreRunE, root.PreRun) + root.PreRun = nil + root.PostRunE = skipOnUsageE(root.PostRunE, root.PostRun) + root.PostRun = nil + root.PersistentPostRunE = skipOnUsageE(root.PersistentPostRunE, root.PersistentPostRun) + root.PersistentPostRun = nil + + runE, run := root.RunE, root.Run + root.Run = nil + root.RunE = func(cmd *cobra.Command, args []string) error { + if UsageRequested(cmd) { + return EmitSchema(cmd, cmd.OutOrStdout()) + } + if runE != nil { + return runE(cmd, args) + } + if run != nil { + run(cmd, args) + return nil + } + return cmd.Help() + } +} + +const usageSynthesizedGroupAnnotation = "speakeasy_usage_synthesized_group" + +func GroupMadeRunnable(cmd *cobra.Command) bool { + return cmd != nil && cmd.Annotations[usageSynthesizedGroupAnnotation] == "true" +} + +// Returning nil rather than a wrapper keeps Cobra's nearest-PersistentPreRunE lookup intact. +func skipOnUsageE(hookE func(*cobra.Command, []string) error, hook func(*cobra.Command, []string)) func(*cobra.Command, []string) error { + if hookE == nil && hook == nil { + return nil + } + return func(cmd *cobra.Command, args []string) error { + if UsageRequested(cmd) { + return nil + } + if hookE != nil { + return hookE(cmd, args) + } + hook(cmd, args) + return nil + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 47c6a84..0000000 --- a/package-lock.json +++ /dev/null @@ -1,763 +0,0 @@ -{ - "name": "@google/gemini-api-cli", - "version": "0.2.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@google/gemini-api-cli", - "version": "0.2.1", - "license": "Apache-2.0", - "dependencies": { - "citty": "^0.1.6", - "dotenv": "^17.4.2", - "js-yaml": "^4.1.0", - "zod": "^3.24.0" - }, - "bin": { - "gemini-api": "dist/cli.js" - }, - "devDependencies": { - "@biomejs/biome": "latest", - "@types/js-yaml": "^4.0.9", - "@types/node": "^22.0.0", - "bun-types": "latest", - "esbuild": "latest", - "typescript": "^5.7.0" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@biomejs/biome": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.13.tgz", - "integrity": "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.13", - "@biomejs/cli-darwin-x64": "2.4.13", - "@biomejs/cli-linux-arm64": "2.4.13", - "@biomejs/cli-linux-arm64-musl": "2.4.13", - "@biomejs/cli-linux-x64": "2.4.13", - "@biomejs/cli-linux-x64-musl": "2.4.13", - "@biomejs/cli-win32-arm64": "2.4.13", - "@biomejs/cli-win32-x64": "2.4.13" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.13.tgz", - "integrity": "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.13.tgz", - "integrity": "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.13.tgz", - "integrity": "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.13.tgz", - "integrity": "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.13", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.13", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.13.tgz", - "integrity": "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.13.tgz", - "integrity": "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "license": "Python-2.0" - }, - "node_modules/bun-types": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.13.tgz", - "integrity": "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/consola": { - "version": "3.4.2", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - }, - "node_modules/zod": { - "version": "3.25.76", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index dcca08f..0000000 --- a/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@google/gemini-api-cli", - "version": "0.2.1", - "description": "CLI to access Gemini API", - "license": "Apache-2.0", - "type": "module", - "bin": { - "gemini-api": "./dist/cli.js" - }, - "files": [ - "dist/cli.js", - "README.md", - "LICENSE" - ], - "scripts": { - "dev": "bun run src/cli.ts", - "build": "bun build ./src/cli.ts --outdir dist --target node --banner=\"#!/usr/bin/env node\"", - "prepare": "node scripts/prepare.js", - "compile": "bun build --compile ./src/cli.ts --outfile dist/gemini-api", - "test": "bun test", - "lint": "biome check .", - "typecheck": "tsc --noEmit" - }, - "engines": { - "node": ">=22" - }, - "dependencies": { - "citty": "^0.1.6", - "dotenv": "^17.4.2", - "js-yaml": "^4.1.0", - "zod": "^3.24.0" - }, - "devDependencies": { - "@types/js-yaml": "^4.0.9", - "@types/node": "^22.0.0", - "bun-types": "latest", - "typescript": "^5.7.0", - "@biomejs/biome": "latest", - "esbuild": "latest" - } -} diff --git a/scripts/compile-all.sh b/scripts/compile-all.sh deleted file mode 100755 index 6734a6a..0000000 --- a/scripts/compile-all.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -set -euo pipefail - -TARGETS=( - "bun-linux-x64:gemini-api-linux-x64" - "bun-linux-arm64:gemini-api-linux-arm64" - "bun-darwin-x64:gemini-api-darwin-x64" - "bun-darwin-arm64:gemini-api-darwin-arm64" - "bun-windows-x64:gemini-api-win-x64.exe" -) - -mkdir -p dist - -for entry in "${TARGETS[@]}"; do - target="${entry%%:*}" - output="${entry##*:}" - echo "Building ${output}..." - bun build --compile --target="${target}" ./src/cli.ts --outfile "dist/${output}" -done - -echo "✓ All binaries built in dist/" -ls -lh dist/ diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..011cb20 --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,151 @@ +# +# gemini-api CLI Installation Script for Windows +# This script downloads and installs the latest version of the gemini-api CLI +# +# Usage: +# iwr -useb https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.ps1 | iex +# or +# Invoke-WebRequest -Uri https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.ps1 -UseBasicParsing | Invoke-Expression +# +# Options: +# $env:GEMINI_INSTALL_DIR - Installation directory (default: $env:LOCALAPPDATA\Programs\gemini-api) +# $env:GEMINI_VERSION - Specific version to install (default: latest) +# + +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +# Configuration +$Repo = "google-gemini/gemini-api-cli" +$BinaryName = "gemini-api.exe" +$DefaultInstallDir = Join-Path $env:LOCALAPPDATA "Programs\gemini-api" +$InstallDir = if ($env:GEMINI_INSTALL_DIR) { $env:GEMINI_INSTALL_DIR } else { $DefaultInstallDir } +$Version = if ($env:GEMINI_VERSION) { $env:GEMINI_VERSION } else { "latest" } + +# Helper functions +function Write-ColorOutput { + param( + [Parameter(Mandatory = $true)] + [string]$Message, + [string]$Color = "White" + ) + Write-Host $Message -ForegroundColor $Color +} + +function Get-LatestVersion { + try { + $response = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -UseBasicParsing + return $response.tag_name + } + catch { + Write-ColorOutput "Failed to get latest version: $_" -Color Red + exit 1 + } +} + +function Get-Architecture { + $arch = $env:PROCESSOR_ARCHITECTURE + switch ($arch) { + "AMD64" { return "x86_64" } + "ARM64" { return "arm64" } + default { + Write-ColorOutput "Unsupported architecture: $arch" -Color Red + exit 1 + } + } +} + +function Install-CLI { + Write-ColorOutput "Installing gemini-api CLI..." -Color Green + + # Detect architecture + $arch = Get-Architecture + Write-ColorOutput "Detected Architecture: $arch" -Color Cyan + + # Get version + if ($Version -eq "latest") { + $Version = Get-LatestVersion + Write-ColorOutput "Latest version: $Version" -Color Cyan + } + + # Construct download URL + $archiveName = "gemini-api_Windows_$arch.zip" + $downloadUrl = "https://github.com/$Repo/releases/download/$Version/$archiveName" + + Write-ColorOutput "Downloading from: $downloadUrl" -Color Cyan + + # Create temporary directory + $tempDir = Join-Path $env:TEMP "gemini-api-install-$(New-Guid)" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + try { + # Download archive + $archivePath = Join-Path $tempDir $archiveName + try { + Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing + } + catch { + Write-ColorOutput "Failed to download from $downloadUrl" -Color Red + Write-ColorOutput "Error: $_" -Color Red + exit 1 + } + + Write-ColorOutput "Download complete" -Color Green + + # Extract archive + Write-ColorOutput "Extracting archive..." -Color Cyan + Expand-Archive -Path $archivePath -DestinationPath $tempDir -Force + + # Create install directory if it doesn't exist + if (-not (Test-Path $InstallDir)) { + Write-ColorOutput "Creating installation directory: $InstallDir" -Color Cyan + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + } + + # Install binary + $binaryPath = Join-Path $InstallDir $BinaryName + Write-ColorOutput "Installing to $binaryPath..." -Color Cyan + + # Remove existing binary if it exists + if (Test-Path $binaryPath) { + Remove-Item $binaryPath -Force + } + + Copy-Item -Path (Join-Path $tempDir $BinaryName) -Destination $binaryPath -Force + + Write-ColorOutput "gemini-api $Version has been installed to $binaryPath" -Color Green + + # Add to PATH if not already there + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($userPath -notlike "*$InstallDir*") { + Write-ColorOutput "Adding $InstallDir to your PATH..." -Color Cyan + [Environment]::SetEnvironmentVariable( + "Path", + "$userPath;$InstallDir", + "User" + ) + $env:Path = "$env:Path;$InstallDir" + Write-ColorOutput "Added to PATH. You may need to restart your terminal for changes to take effect." -Color Yellow + } + + Write-ColorOutput "Installation successful! Run 'gemini-api --help' to get started." -Color Green + Write-ColorOutput "Note: You may need to restart your terminal or run 'refreshenv' for the PATH changes to take effect." -Color Yellow + } + finally { + # Cleanup + if (Test-Path $tempDir) { + Remove-Item $tempDir -Recurse -Force + } + } +} + +# Main execution +try { + Install-CLI +} +catch { + Write-ColorOutput "Installation failed: $_" -Color Red + exit 1 +} diff --git a/scripts/install.sh b/scripts/install.sh index 4e604da..3ae0094 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,85 +1,230 @@ -#!/bin/bash -# Copyright 2026 Google LLC +#!/usr/bin/env bash # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# gemini-api CLI Installation Script +# This script downloads and installs the latest version of the gemini-api CLI +# for Linux and macOS systems. # -# https://www.apache.org/licenses/LICENSE-2.0 +# Usage: +# curl -fsSL https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.sh | bash +# or +# wget -qO- https://raw.githubusercontent.com/google-gemini/gemini-api-cli/main/scripts/install.sh | bash # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# install.sh — Install gemini-api CLI -set -euo pipefail - -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m) - -# Map arch names -case "$ARCH" in - x86_64) ARCH="x64" ;; - aarch64|arm64) ARCH="arm64" ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; -esac - -# Map OS names -case "$OS" in - linux) OS="linux" ;; - darwin) OS="darwin" ;; - *) - echo "Unsupported OS: $OS" - exit 1 - ;; -esac - -BINARY="gemini-api-${OS}-${ARCH}" +# Options: +# GEMINI_INSTALL_DIR - Installation directory (default: /usr/local/bin) +# GEMINI_VERSION - Specific version to install (default: latest) +# + +set -e + +# Configuration REPO="google-gemini/gemini-api-cli" -URL="https://github.com/${REPO}/releases/latest/download/${BINARY}" +DEFAULT_INSTALL_DIR="/usr/local/bin" +USER_INSTALL_DIR="$HOME/.local/bin" +VERSION="${GEMINI_VERSION:-latest}" +BINARY_NAME="gemini-api" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Detect operating system +detect_os() { + local os + local uname_output="$(uname -s)" + case "$uname_output" in + Linux*) os="Linux" ;; + Darwin*) os="Darwin" ;; + CYGWIN*|MINGW*|MSYS*) os="Windows" ;; + *) + log_error "Unsupported operating system: $uname_output" + exit 1 + ;; + esac + echo "$os" +} + +# Detect architecture +detect_arch() { + local arch + case "$(uname -m)" in + x86_64|amd64) arch="x86_64" ;; + aarch64|arm64) arch="arm64" ;; + *) + log_error "Unsupported architecture: $(uname -m)" + exit 1 + ;; + esac + echo "$arch" +} + +# Get latest version from GitHub +get_latest_version() { + local latest_url="https://api.github.com/repos/${REPO}/releases/latest" + local version + + if command -v curl >/dev/null 2>&1; then + version=$(curl -fsSL "$latest_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + elif command -v wget >/dev/null 2>&1; then + version=$(wget -qO- "$latest_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + else + log_error "curl or wget is required to download the CLI" + exit 1 + fi + + echo "$version" +} # Determine installation directory -if [ -w "/usr/local/bin" ]; then - DEST_DIR="/usr/local/bin" -else - DEST_DIR="$HOME/.local/bin" - mkdir -p "$DEST_DIR" -fi - -DEST="${DEST_DIR}/gemini-api" - -echo "Downloading ${BINARY} from GitHub releases..." -if command -v curl >/dev/null 2>&1; then - curl -fsSL "$URL" -o "$DEST" -elif command -v wget >/dev/null 2>&1; then - wget -qO "$DEST" "$URL" -else - echo "Error: curl or wget is required to download the binary." - exit 1 -fi - -chmod +x "$DEST" - -echo "✓ Installed gemini-api to ${DEST}" - -# Check if DEST_DIR is in PATH -case ":$PATH:" in - *":$DEST_DIR:"*) ;; - *) - echo "Warning: $DEST_DIR is not in your PATH." - echo "You may need to add it to your shell profile (e.g., ~/.bashrc or ~/.zshrc):" - echo " export PATH=\"\$PATH:$DEST_DIR\"" - ;; -esac - -# Verify installation if it's in PATH -if command -v gemini-api >/dev/null 2>&1; then - echo "Verification: $(gemini-api --version)" -else - echo "To run it, you may need to restart your terminal or use the full path:" - echo " $DEST --version" -fi +get_install_dir() { + # If user specified a directory, use it + if [ -n "${GEMINI_INSTALL_DIR}" ]; then + echo "${GEMINI_INSTALL_DIR}" + return + fi + + # Try to use /usr/local/bin if we have write access + if [ -w "$DEFAULT_INSTALL_DIR" ] || [ -w "$(dirname "$DEFAULT_INSTALL_DIR")" ]; then + echo "$DEFAULT_INSTALL_DIR" + return + fi + + # Fall back to user directory + log_info "No write access to $DEFAULT_INSTALL_DIR, using $USER_INSTALL_DIR instead" >&2 + echo "$USER_INSTALL_DIR" +} + +# Download and install +install_cli() { + local INSTALL_DIR=$(get_install_dir) + local os=$(detect_os) + local arch=$(detect_arch) + + log_info "Detected OS: $os" + log_info "Detected Architecture: $arch" + log_info "Installation directory: $INSTALL_DIR" + + # Get version + if [ "$VERSION" = "latest" ]; then + VERSION=$(get_latest_version) + log_info "Latest version: $VERSION" + fi + + # Construct download URL based on OS + local archive_name + local archive_format + if [ "$os" = "Windows" ]; then + archive_name="${BINARY_NAME}_${os}_${arch}.zip" + archive_format="zip" + else + archive_name="${BINARY_NAME}_${os}_${arch}.tar.gz" + archive_format="tar.gz" + fi + + local download_url="https://github.com/${REPO}/releases/download/${VERSION}/${archive_name}" + + log_info "Downloading from: $download_url" + + # Create temporary directory + local tmp_dir=$(mktemp -d) + trap "rm -rf $tmp_dir" EXIT + + # Download archive + if command -v curl >/dev/null 2>&1; then + if ! curl -fsSL "$download_url" -o "$tmp_dir/$archive_name"; then + log_error "Failed to download from $download_url" + exit 1 + fi + elif command -v wget >/dev/null 2>&1; then + if ! wget -q "$download_url" -O "$tmp_dir/$archive_name"; then + log_error "Failed to download from $download_url" + exit 1 + fi + fi + + log_info "Download complete" + + # Extract archive based on format + log_info "Extracting archive..." + if [ "$archive_format" = "zip" ]; then + if command -v unzip >/dev/null 2>&1; then + unzip -q "$tmp_dir/$archive_name" -d "$tmp_dir" + else + log_error "unzip is required to extract the archive. Please install unzip and try again." + exit 1 + fi + else + tar -xzf "$tmp_dir/$archive_name" -C "$tmp_dir" + fi + + # Create install directory if it doesn't exist + if [ ! -d "$INSTALL_DIR" ]; then + log_info "Creating installation directory: $INSTALL_DIR" + mkdir -p "$INSTALL_DIR" || { + log_error "Failed to create $INSTALL_DIR. Try running with sudo or set GEMINI_INSTALL_DIR to a writable location." + exit 1 + } + fi + + # Install binary (Windows binaries have .exe extension) + local source_binary="$tmp_dir/$BINARY_NAME" + local target_binary="$INSTALL_DIR/$BINARY_NAME" + + if [ "$os" = "Windows" ]; then + source_binary="$tmp_dir/${BINARY_NAME}.exe" + target_binary="$INSTALL_DIR/${BINARY_NAME}.exe" + fi + + log_info "Installing to $target_binary..." + if ! mv "$source_binary" "$target_binary"; then + log_error "Failed to install to $INSTALL_DIR. Try running with sudo or set GEMINI_INSTALL_DIR to a writable location." + exit 1 + fi + + # Make executable (not needed on Windows, but doesn't hurt) + chmod +x "$target_binary" 2>/dev/null || true + + log_info "gemini-api ${VERSION} has been installed to $target_binary" + + # Verify installation + local cmd_to_check="$BINARY_NAME" + if [ "$os" = "Windows" ]; then + cmd_to_check="${BINARY_NAME}.exe" + fi + + if command -v "$cmd_to_check" >/dev/null 2>&1; then + log_info "Installation successful! Run '$BINARY_NAME --help' to get started." + else + log_warn "Installation complete, but $BINARY_NAME is not in your PATH." + if [ "$os" = "Windows" ]; then + log_warn "Add $INSTALL_DIR to your PATH environment variable." + else + log_warn "Add $INSTALL_DIR to your PATH by adding this to your ~/.bashrc or ~/.zshrc:" + log_warn " export PATH=\"\$PATH:$INSTALL_DIR\"" + log_warn "" + log_warn "Then run: source ~/.bashrc # or source ~/.zshrc" + fi + fi +} + +# Main execution +main() { + log_info "Installing gemini-api CLI..." + install_cli +} + +main diff --git a/scripts/prepare.js b/scripts/prepare.js deleted file mode 100644 index 70a8a16..0000000 --- a/scripts/prepare.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { execSync } from "node:child_process"; - -function hasBun() { - try { - execSync("bun --version", { stdio: "ignore" }); - return true; - } catch (_e) { - return false; - } -} - -if (hasBun()) { - console.log("Bun found. Building with Bun..."); - execSync("bun run build", { stdio: "inherit" }); -} else { - console.log("Bun not found. Falling back to esbuild..."); - try { - execSync( - 'npx -y esbuild src/cli.ts --bundle --platform=node --outdir=dist "--banner:js=#!/usr/bin/env node"', - { stdio: "inherit" }, - ); - } catch (_e) { - console.error("Failed to build with esbuild."); - process.exit(1); - } -} diff --git a/skills/gemini-api-cli/SKILL.md b/skills/gemini-api-cli/SKILL.md deleted file mode 100644 index fc94aaa..0000000 --- a/skills/gemini-api-cli/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: gemini-api-cli -description: Guide for using the Gemini API CLI tool. Use when you need to interact with the Gemini API via the command line, manage agents, or generate media (images, audio). ---- - -# Gemini API CLI Skill - -This skill provides guidance on using the `gemini-api` command-line interface. - -## Overview - -The `gemini-api` CLI allows you to: -- Run prompts against various Gemini models. -- Manage the full lifecycle of Gemini Agents. -- Generate and edit media (images, audio, TTS). - -## References - -For detailed usage and examples, see the following references: - -- **Normal Model Calls**: See [references/model_calls.md](references/model_calls.md) for text generation, multi-turn conversations, and tool usage. -- **Agents (Lifecycle)**: See [references/agents.md](references/agents.md) for creating, testing, and managing agents. -- **Agent Calls**: See [references/agent_calls.md](references/agent_calls.md) for invoking antigravity-preview-05-2026 and Deep Research agents. -- **Genmedia**: See [references/genmedia.md](references/genmedia.md) for image generation, image editing, and text-to-speech. - -## Basic Usage - -The primary command is `gemini-api run`. - -```bash -gemini-api run "Hello, who are you?" -``` - -Always ensure your `GEMINI_API_KEY` environment variable is set. - -## Global Flags & Features - -The CLI supports several flags that are useful for debugging and automation: - -### Dry Run (`--dry-run`) -Prints the equivalent `curl` command and exits without making an API call. Useful for verifying what request would be sent. -```bash -gemini-api run "Hello" --dry-run -``` - -### Help (`--help`) -Displays usage information and available flags for any command. -```bash -gemini-api --help -gemini-api run --help -``` - -### JSON Output (`--json`) -Outputs raw SSE events as JSONL (one event per line), useful for machine parsing. -```bash -gemini-api run "Hello" --json -``` - -### Verbose (`--verbose` / `-v`) -Outputs complete JSON step objects line-by-line as they finish. Recommended for programmatic parsing by calling agents. -```bash -gemini-api run "Hello" --verbose -``` diff --git a/skills/gemini-api-cli/references/agent_calls.md b/skills/gemini-api-cli/references/agent_calls.md deleted file mode 100644 index 5f6c2f7..0000000 --- a/skills/gemini-api-cli/references/agent_calls.md +++ /dev/null @@ -1,50 +0,0 @@ -# Agent Calls - -This reference covers how to interact with Gemini Agents using the `gemini-api run` command. - -## antigravity-preview-05-2026 (Custom Agents) - -When you create a custom agent (which defaults to using `antigravity-preview-05-2026` as the base agent), you interact with it using the `--agent` flag followed by the agent ID. - -```bash -gemini-api run "Analyze this dataset for trends" --agent my-custom-agent -``` - -If the agent requires files in its workspace, ensure they are provided or already uploaded. - -### Passing Environment Sources - -When using `--agent antigravity-preview-05-2026` (or a custom agent based on it), you can pass environment sources using the `--source` flag. This allows you to seed files or clone repositories into the agent's environment. Custom sources override the default auto-enabled environment. - -```bash -gemini-api run "Generate a video" \ - --agent antigravity-preview-05-2026 \ - --source "inline:/.agents/README.md:# Instructions" \ - --source "github:https://github.com/user/repo:/.agents" -``` - -Supported source types: -- `inline::`: Creates a file with the specified content. -- `github::`: Clones a GitHub repository to the target path. -- `gcs::`: Copies files from Google Cloud Storage to the target path. - -## Deep Research - -Deep Research is a specialized agent designed for long-running, complex research tasks. You can invoke it using the specific agent ID listed in the models/agents table. - -Invoke the latest Deep Research agent: - -```bash -gemini-api run "Research the latest developments in room-temperature superconductors" \ - --agent deep-research-preview-04-2026 -``` - -Invoke Deep Research Max (for more extensive research): - -```bash -gemini-api run "Provide a comprehensive analysis of the global semiconductor supply chain" \ - --agent deep-research-max-preview-04-2026 -``` - -> [!NOTE] -> Deep Research queries often take longer to complete and may involve multiple steps and tool usage (like web search) automatically. diff --git a/skills/gemini-api-cli/references/agents.md b/skills/gemini-api-cli/references/agents.md deleted file mode 100644 index d4a3c15..0000000 --- a/skills/gemini-api-cli/references/agents.md +++ /dev/null @@ -1,143 +0,0 @@ -# Gemini Agents - -This reference covers managing the lifecycle of Gemini Agents using the CLI. - -## Scaffolding a New Agent - -Create a new agent project directory: - -```bash -gemini-api agents init my-agent -``` - -This creates a directory with the default structure. - -### Agent Directory Structure - -``` -my-agent/ -├── agent.yaml # Configuration (ID, tools, etc.) — not inlined -├── AGENTS.md # System instructions (inlined to /.agents/AGENTS.md) -├── skills/ # Custom skills (all files inlined recursively) -└── workspace/ # Files seeded into remote environment (all files inlined recursively) -``` - -> **Note:** Only `AGENTS.md`, `workspace/`, and `skills/` are inlined from the agent directory. All other root-level files and directories (e.g., `README.md`, `package.json`) are ignored during `agents test` and `agents create`. - -### Adding Skills - -To add a skill to your agent, place the skill files (e.g., `SKILL.md`, `references/`) in the `skills/` directory of your agent project. The agent will have access to these skills when running in its environment. - -### Agent Configuration (`agent.yaml`) - -Required fields: -- `id`: Agent identifier -- `base_agent`: Base model (Supported only: `antigravity-preview-05-2026`) - -Optional fields: -- `description`: Description of the agent -- `system_instruction`: Short system instruction (prefer `AGENTS.md` for long instructions) -- `tools`: List of tools (e.g., `code_execution`, `google_search`) -- `environment`: `"remote"`, a base environment ID, or a structured config object to configure sources, network allowlists, and secrets injection: - ```yaml - environment: - type: "remote" - sources: - - type: "gcs" - source: "gs://my-bucket/folder/" - target: ".agents/workspace" - network: - allowlist: - - domain: "api.github.com" - transform: - Authorization: "Bearer your-github-token" - - domain: "*" # Catch-all - ``` - -## Testing an Agent Locally - -You can test your agent configuration locally before creating it on the platform: - -```bash -cd my-agent -gemini-api agents test --prompt "Hello, what are your instructions?" -``` - -Or specify the path: - -```bash -gemini-api agents test --prompt "Hello" --path ./my-agent -``` - -## Creating/Deploying an Agent - -Deploy the agent to the platform: - -```bash -cd my-agent -gemini-api agents create -``` - -This will output the agent ID and environment ID. - -## Running an Interaction with an Agent - -Once created, you can run prompts against the agent: - -```bash -gemini-api run "Analyze my data" --agent my-agent -``` - -## Listing Agents - -List all agents you have created: - -```bash -gemini-api agents list -``` - -Use `--json` for machine-readable output: - -```bash -gemini-api agents list --json -``` - -## Deleting an Agent - -Delete an agent from the platform: - -```bash -gemini-api agents delete my-agent -``` - -Use `--force` to skip confirmation: - -```bash -gemini-api agents delete my-agent --force -``` - -## Downloading Files from Agent Environment - -You can download all files from an agent's environment (e.g., generated data, logs) as a snapshot: - -```bash -gemini-api files download -``` - -Specify an output directory: - -```bash -gemini-api files download --output ./results -``` - -## Handling Long-Running Tests - -Tests initiated via `agents test` can take multiple minutes to complete. -- **Do Not Interrupt**: Be patient and allow the command to run. -- **Streaming**: Streaming is enabled by default. The CLI will stream output live. - -## Best Practices - -- Always check that the current directory contains `agent.yaml` before running local commands. -- Use `--dry-run` to see what the CLI would do without executing. -- Prefer `AGENTS.md` for detailed instructions rather than putting them all in `agent.yaml`'s `system_instruction`. diff --git a/skills/gemini-api-cli/references/genmedia.md b/skills/gemini-api-cli/references/genmedia.md deleted file mode 100644 index 2104399..0000000 --- a/skills/gemini-api-cli/references/genmedia.md +++ /dev/null @@ -1,56 +0,0 @@ -# Genmedia (Image, Audio, TTS) - -This reference covers generating and editing media using the Gemini API CLI. - -## Image Generation - -To generate an image, use a model that supports image generation and specify an output file with `--output` or `-o`. - -```bash -gemini-api run "A cat in space, oil painting" --model gemini-3.1-flash-image --output cat.png -``` - -Supported image models include: -- `gemini-3-pro-image` (Nano Banana Pro) -- `gemini-3.1-flash-image` (Nano Banana 2) -- `gemini-2.5-flash-image` - -## Image Editing - -You can edit an existing image by providing it as input and describing the changes: - -```bash -gemini-api run "Add a red hat to the person" \ - --input image:person.jpg \ - --response-modality image \ - --output with_hat.jpg -``` - -## Text-to-Speech (TTS) - -To generate audio from text, use a TTS model and specify an audio output file: - -```bash -gemini-api run "Hello, I can help you with a wide range of tasks." \ - --model gemini-3.1-flash-tts-preview \ - --voice Kore \ - --output hello.wav -``` - -Supported TTS models: -- `gemini-3.1-flash-tts-preview` -- `gemini-2.5-flash-preview-tts` -- `gemini-2.5-pro-preview-tts` - -## Music Generation (Lyria) - -The CLI supports Lyria models for music generation, although specific command examples are minimal. You would typically use the `run` command with a Lyria model and an appropriate output extension (e.g., `.wav` or `.mp3`). - -Supported Lyria models: -- `lyria-3-clip-preview` (Music clips) -- `lyria-3-pro-preview` (Full-song generation) - -Example (inferred usage): -```bash -gemini-api run "A happy upbeat electronic track" --model lyria-3-pro-preview --output song.wav -``` diff --git a/skills/gemini-api-cli/references/model_calls.md b/skills/gemini-api-cli/references/model_calls.md deleted file mode 100644 index f5e97d2..0000000 --- a/skills/gemini-api-cli/references/model_calls.md +++ /dev/null @@ -1,61 +0,0 @@ -# Normal Model Calls - -This reference covers standard interactions with Gemini models using the `gemini-api` CLI. - -## Basic Prompting - -Run a simple prompt against the default model (`gemini-3.5-flash`): - -```bash -gemini-api run "What is the capital of France?" -``` - -## Specifying a Model - -Use the `--model` or `-m` flag to specify a different model: - -```bash -gemini-api run "Explain quantum computing" --model gemini-3.1-pro-preview -``` - -Common models: -- `gemini-3.5-flash` (default) -- `gemini-3.1-pro-preview` -- `gemini-3.1-flash-lite` - -## Multi-Turn Conversations - -To continue a conversation, use the `--previous-interaction-id` or `-p` flag with the ID returned from the previous command. - -```bash -# First turn -gemini-api run "Remember the word: banana" -# Output will include: interaction_id: v1_... - -# Second turn -gemini-api run "What word did I tell you to remember?" --previous-interaction-id v1_... -``` - -## Using Tools - -You can enable tools like Google Search or Code Execution: - -```bash -gemini-api run "What is the current weather in Tokyo?" --tool google_search -gemini-api run "Calculate the 100th Fibonacci number" --tool code_execution -``` - -You can repeat the `--tool` flag to enable multiple tools: - -```bash -gemini-api run "Research and analyze the latest AI trends" --tool google_search --tool code_execution -``` - -## Multimodal Input - -Pass files as input using the `--input` or `-i` flag: - -```bash -gemini-api run "Describe this image" --input image:path/to/photo.jpg -gemini-api run "Summarize this document" --input document:path/to/report.pdf -``` diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index 6e3a2d3..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// gemini-api CLI entry point -// TODO: Implement — see tasks/task_2.md - -import { defineCommand, runMain } from "citty"; - -const main = defineCommand({ - meta: { - name: "gemini-api", - version: "0.2.1", - description: "CLI to access Gemini API", - }, - subCommands: { - run: () => import("./commands/run").then((m) => m.default), - agents: () => import("./commands/agents/index").then((m) => m.default), - files: () => import("./commands/files/index").then((m) => m.default), - }, -}); - -if (process.argv.includes("--help") || process.argv.includes("-h")) { - process.env.CONSOLA_LEVEL = "5"; -} - -runMain(main); diff --git a/src/commands/agents/create.ts b/src/commands/agents/create.ts deleted file mode 100644 index 1468ae7..0000000 --- a/src/commands/agents/create.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { defineCommand } from "citty"; -import { - apiRequest, - normalizeSources, - resolveContext, - type Source, - validateSources, -} from "../../lib/api"; -import { loadAgent } from "../../lib/config"; -import { CLIError, ConfigError } from "../../lib/errors"; -import { collectInlineFiles } from "../../lib/files"; -import { printCurl, printError } from "../../lib/output"; -import { globalFlags } from "../../lib/shared-args"; - -export default defineCommand({ - meta: { - name: "create", - description: `Deploy the agent from the current directory to the platform. - -Examples: - gemini-api agents create - gemini-api agents create --path ./my-agent - gemini-api agents create --dry-run`, - }, - args: { - ...globalFlags, - path: { - type: "string", - description: "Path to agent directory", - default: ".", - }, - "base-env": { - type: "string", - description: "Override base environment", - }, - env: { - type: "string", - alias: "e", - description: "Load environment variables from a .env file", - }, - }, - async run({ args }) { - try { - const agentDir = args.path as string; - const baseEnvOverride = args["base-env"] as string | undefined; - const envFile = args.env as string | undefined; - const ctx = resolveContext(args); - - const { config } = await loadAgent(agentDir, { envFile }); - - const body: Record = { - id: config.id, - base_agent: config.base_agent, - }; - - // Only send base_agent and instructions for now to debug - /* - if (config.description) body.description = config.description; - if (config.tools) body.tools = config.tools; - if (config.subagents) body.subagents = config.subagents; - if (config.metadata) body.metadata = config.metadata; - */ - - if (config.instructions) { - body.system_instruction = config.instructions; - } - - // Handle base_environment - if (baseEnvOverride) { - body.base_environment = baseEnvOverride; - } else if (config.base_environment) { - if (typeof config.base_environment === "string") { - body.base_environment = config.base_environment; - } else if (typeof config.base_environment === "object") { - const baseEnvObj = config.base_environment as any; - if (baseEnvObj.sources) { - const normalizedBaseSources = normalizeSources(baseEnvObj.sources); - validateSources(normalizedBaseSources); - baseEnvObj.sources = normalizedBaseSources; - } - body.base_environment = baseEnvObj; - } - } else { - // Collect and inline files from the agent directory - const inlineFiles = await collectInlineFiles(agentDir); - - const sources: Source[] = [...inlineFiles] as unknown as Source[]; - if (config.sources) { - sources.push(...config.sources); - } - - // Also merge sources from environment.sources (e.g. gcs, github) - const env = config.environment as Record | undefined; - if (env && env.type === "remote" && Array.isArray(env.sources)) { - sources.push(...(env.sources as Source[])); - } - - const normalized = normalizeSources(sources); - validateSources(normalized); - - if (normalized && normalized.length > 0) { - body.base_environment = { - type: "remote", - sources: normalized, - }; - const envObj = config.environment as any; - if (envObj && typeof envObj === "object" && envObj.network) { - (body.base_environment as any).network = envObj.network; - } - } - } - - const url = "/agents"; - - if (args["dry-run"]) { - printCurl("POST", `${ctx.baseUrl}${url}`, ctx.apiKey, body); - return; - } - - const response = await apiRequest(ctx, "POST", url, body); - - if (args.json) { - console.log(JSON.stringify(response, null, 2)); - } else { - console.log(`✓ Created agent: ${response.name || response.id}`); - const agentId = response.id || (response.name ? response.name.split("/").pop() : "unknown"); - console.log(` agent_id: ${agentId}`); - console.log(` base_agent: ${response.base_agent}`); - if (response.environment) { - console.log(` environment: ${response.environment}`); - } - } - } catch (error) { - if (error instanceof CLIError || error instanceof ConfigError) { - printError(error.message); - } else { - printError(`Unexpected error: ${(error as Error).message}`); - } - process.exit(1); - } - }, -}); diff --git a/src/commands/agents/delete.ts b/src/commands/agents/delete.ts deleted file mode 100644 index 62e169d..0000000 --- a/src/commands/agents/delete.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as readline from "node:readline"; -import { defineCommand } from "citty"; -import { apiRequest, resolveContext } from "../../lib/api"; -import { CLIError } from "../../lib/errors"; -import { printCurl, printError } from "../../lib/output"; -import { globalFlags } from "../../lib/shared-args"; - -export default defineCommand({ - meta: { - name: "delete", - description: `Delete deployed agent. - -Examples: - gemini-api agents delete my-agent - gemini-api agents delete my-agent --force - gemini-api agents delete my-agent --dry-run`, - }, - args: { - ...globalFlags, - id: { - type: "positional", - description: "Agent ID", - required: true, - }, - force: { - type: "boolean", - description: "Skip confirmation", - default: false, - }, - }, - async run({ args }) { - try { - const ctx = resolveContext(args); - const id = args.id; - const force = args.force; - - if (!id || id.trim() === "") { - printError("Error: Agent ID cannot be empty."); - process.exit(1); - } - - const url = `/agents/${id}`; - - if (args["dry-run"]) { - printCurl("DELETE", `${ctx.baseUrl}${url}`, ctx.apiKey); - return; - } - - const performDelete = async () => { - const response = await apiRequest(ctx, "DELETE", url); - if (args.json) { - console.log(JSON.stringify(response, null, 2)); - } else { - console.log(`✓ Deleted agent: agents/${id}`); - } - }; - - if (force || !process.stdout.isTTY) { - await performDelete(); - return; - } - - const confirmed = await new Promise((resolve) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.question( - `⚠ This will permanently delete agent '${id}'.\n Are you sure? [y/N] `, - (answer) => { - rl.close(); - resolve(answer.toLowerCase() === "y"); - }, - ); - }); - - if (confirmed) { - await performDelete(); - } else { - console.log("Aborted."); - } - } catch (error) { - if (error instanceof CLIError) { - printError(error.message); - } else { - printError(`Unexpected error: ${(error as Error).message}`); - } - process.exit(1); - } - }, -}); diff --git a/src/commands/agents/get.ts b/src/commands/agents/get.ts deleted file mode 100644 index 93ce1eb..0000000 --- a/src/commands/agents/get.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { defineCommand } from "citty"; -import * as yaml from "js-yaml"; -import { apiRequest, resolveContext } from "../../lib/api"; -import { CLIError } from "../../lib/errors"; -import { printCurl, printError } from "../../lib/output"; -import { globalFlags } from "../../lib/shared-args"; - -export default defineCommand({ - meta: { - name: "get", - description: `Get agent details. - -Examples: - gemini-api agents get my-agent - gemini-api agents get my-agent --json - gemini-api agents get my-agent --dry-run`, - }, - args: { - ...globalFlags, - id: { - type: "positional", - description: "Agent ID", - required: true, - }, - }, - async run({ args }) { - try { - const ctx = resolveContext(args); - const url = `/agents/${args.id}`; - - if (args["dry-run"]) { - printCurl("GET", `${ctx.baseUrl}${url}`, ctx.apiKey); - return; - } - - const response = await apiRequest(ctx, "GET", url); - - if (args.json) { - console.log(JSON.stringify(response, null, 2)); - } else { - console.log(yaml.dump(response)); - } - } catch (error) { - if (error instanceof CLIError) { - printError(error.message); - } else { - printError(`Unexpected error: ${(error as Error).message}`); - } - process.exit(1); - } - }, -}); diff --git a/src/commands/agents/index.ts b/src/commands/agents/index.ts deleted file mode 100644 index c4d67ef..0000000 --- a/src/commands/agents/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// gemini-api agents subcommand group -// TODO: Implement — see tasks/task_8.md - -import { defineCommand } from "citty"; - -export default defineCommand({ - meta: { - name: "agents", - description: "Manage the agent lifecycle: init, create, list, get, delete, test", - }, - subCommands: { - init: () => import("./init").then((m) => m.default), - create: () => import("./create").then((m) => m.default), - list: () => import("./list").then((m) => m.default), - get: () => import("./get").then((m) => m.default), - delete: () => import("./delete").then((m) => m.default), - test: () => import("./test").then((m) => m.default), - }, -}); diff --git a/src/commands/agents/init.ts b/src/commands/agents/init.ts deleted file mode 100644 index 9b24c5f..0000000 --- a/src/commands/agents/init.ts +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { execSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { defineCommand } from "citty"; -import * as yaml from "js-yaml"; -import { globalFlags } from "../../lib/shared-args"; - -export default defineCommand({ - meta: { - name: "init", - description: `Scaffold new agent project. - -Examples: - gemini-api agents init my-agent - gemini-api agents init my-agent --base-agent antigravity-preview-05-2026`, - }, - args: { - ...globalFlags, - name: { - type: "positional", - description: "Name of the agent project", - required: true, - }, - "base-agent": { - type: "string", - description: "Base model to set in agent.yaml", - default: "antigravity-preview-05-2026", - }, - "from-template": { - type: "string", - description: "Git or GCS URL to scaffold from", - }, - }, - run({ args }) { - const name = args.name; - const baseAgent = args["base-agent"]; - const fromTemplate = args["from-template"]; - const dryRun = args["dry-run"]; - - if (!name || name.trim() === "") { - console.error("Error: Agent name cannot be empty."); - process.exit(1); - } - - if (fs.existsSync(name)) { - console.log(`Directory '${name}' already exists.`); - return; - } - - if (dryRun) { - console.log(`[dry-run] Would create directory: ${name}`); - console.log(`[dry-run] Would create directory: ${path.join(name, "workspace")}`); - console.log(`[dry-run] Would create file: ${path.join(name, "agent.yaml")}`); - console.log(`[dry-run] Would create file: ${path.join(name, "AGENTS.md")}`); - if (fromTemplate) { - console.log(`[dry-run] Would scaffold from template: ${fromTemplate}`); - } - return; - } - - if (fromTemplate) { - console.log(`Scaffolding from template: ${fromTemplate}...`); - try { - // Simple git clone for now if it looks like a git URL or github repo - if (fromTemplate.startsWith("http") || fromTemplate.startsWith("git@")) { - execSync(`source ~/.bash_profile && git clone ${fromTemplate} ${name}`, { - stdio: "inherit", - }); - console.log(`✓ Scaffolded from template ${fromTemplate}`); - return; - } else { - console.error(`Error: Unsupported template URL format: ${fromTemplate}`); - process.exit(1); - } - } catch (error) { - console.error(`Error scaffolding from template: ${error}`); - process.exit(1); - } - } - - fs.mkdirSync(name, { recursive: true }); - fs.mkdirSync(path.join(name, "workspace"), { recursive: true }); - fs.mkdirSync(path.join(name, "skills"), { recursive: true }); - - const agentConfig = { - id: name, - base_agent: baseAgent, - description: `Scaffolded agent ${name}`, - tools: [{ type: "code_execution" }], - environment: "remote", - }; - - const baseYaml = yaml.dump(agentConfig); - const helperComments = ` -# --- Advanced Environment Configuration (Optional) --- -# Uncomment and customize to configure GCS/GitHub sources, external network access, -# and secret credentials via header transforms. -# -# environment: -# type: "remote" -# # Sources to copy or clone into the environment on startup -# sources: -# - type: "gcs" -# source: "gs://my-bucket-name/folder/" -# target: ".agents/workspace" -# - type: "github" -# source: "https://github.com/my-username/my-repo" -# target: ".agents/workspace/repo" -# -# # Configure outbound network policies and inject secrets securely -# network: -# allowlist: -# - domain: "api.github.com" -# transform: -# Authorization: "Bearer your-github-token" -# - domain: "storage.googleapis.com" -# transform: -# Authorization: "Bearer your-gcloud-oauth-token" -# - domain: "*.wikipedia.org" -# # Optional catch-all entry to allow other traffic without header injection -# - domain: "*" -`; - fs.writeFileSync(path.join(name, "agent.yaml"), baseYaml + helperComments, "utf-8"); - - const STARTER_AGENTS_MD = `# Agent Instructions - -Describe your agent's behavior, personality, and capabilities here. -This file is uploaded to the agent environment and merged with system_instruction on the server. - -## What This Agent Does - - - -## Rules - - - -`; - - fs.writeFileSync(path.join(name, "AGENTS.md"), STARTER_AGENTS_MD, "utf-8"); - - console.log(`✓ Initialized agent project in ${name}/`); - console.log(); - console.log(` ${name}/`); - console.log(` ├── agent.yaml # Agent configuration`); - console.log(` ├── AGENTS.md # System instructions`); - console.log(` ├── skills/ # Custom skills`); - console.log(` └── workspace/ # Files seeded into environment`); - console.log(); - console.log(`Next: cd ${name} && gemini-api agents test --prompt "Hello"`); - }, -}); diff --git a/src/commands/agents/list.ts b/src/commands/agents/list.ts deleted file mode 100644 index 5297e46..0000000 --- a/src/commands/agents/list.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { defineCommand } from "citty"; -import { apiRequest, resolveContext } from "../../lib/api"; -import { CLIError } from "../../lib/errors"; -import { printCurl, printError } from "../../lib/output"; -import { globalFlags } from "../../lib/shared-args"; - -export default defineCommand({ - meta: { - name: "list", - description: `List deployed agents. - -Examples: - gemini-api agents list - gemini-api agents list --json - gemini-api agents list --dry-run`, - }, - args: { - ...globalFlags, - }, - async run({ args }) { - try { - const ctx = resolveContext(args); - const url = "/agents?pageSize=100"; - - if (args["dry-run"]) { - printCurl("GET", `${ctx.baseUrl}${url}`, ctx.apiKey); - return; - } - - const response = await apiRequest(ctx, "GET", url); - - const agents = Array.isArray(response) ? response : response.agents || []; - - if (args.json) { - console.log(JSON.stringify(response, null, 2)); - } else { - if (agents.length === 0) { - console.log("No agents found."); - return; - } - - console.log(`${"Name".padEnd(15) + "Base Agent".padEnd(15)}Created`); - console.log(`${"─".repeat(14)} ${"─".repeat(14)} ${"─".repeat(12)}`); - - for (const agent of agents) { - const name = agent.id || agent.name || ""; - const baseAgent = agent.base_agent || ""; - const created = agent.created_time || agent.createTime || ""; - const createdDate = created ? new Date(created).toISOString().split("T")[0] : ""; - - console.log(name.padEnd(15) + baseAgent.padEnd(15) + createdDate); - } - } - } catch (error) { - if (error instanceof CLIError) { - printError(error.message); - } else { - printError(`Unexpected error: ${(error as Error).message}`); - } - process.exit(1); - } - }, -}); diff --git a/src/commands/agents/test.ts b/src/commands/agents/test.ts deleted file mode 100644 index 225a942..0000000 --- a/src/commands/agents/test.ts +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { defineCommand } from "citty"; -import { - apiStreamRequest, - buildInteractionRequest, - isAgentName, - normalizeSources, - type RunOptions, - resolveContext, - type Source, - validateSources, -} from "../../lib/api"; -import { loadAgent } from "../../lib/config"; -import { CLIError, ConfigError } from "../../lib/errors"; -import { collectInlineFiles } from "../../lib/files"; -import { logRequest, logResponse } from "../../lib/logger"; -import { - HumanStreamRenderer, - mapContentToStepEvent, - printCompletionSummary, - printCurl, - printError, - renderStepEvent, -} from "../../lib/output"; -import { globalFlags } from "../../lib/shared-args"; -import { processStream } from "../../lib/stream"; - -export default defineCommand({ - meta: { - name: "test", - description: `Run interaction against local agent config. - -Examples: - gemini-api agents test --prompt "Hello" - gemini-api agents test --prompt "Hello" --path ./my-agent`, - }, - args: { - ...globalFlags, - prompt: { - type: "string", - description: "Input prompt", - required: true, - }, - path: { - type: "string", - description: "Path to agent directory", - default: ".", - }, - "previous-interaction-id": { - type: "string", - description: "Continue from previous interaction", - }, - environment: { - type: "string", - description: "Use existing environment", - }, - env: { - type: "string", - alias: "e", - description: "Load environment variables from a .env file", - }, - }, - async run({ args }) { - try { - const agentDir = args.path as string; - const prompt = args.prompt as string; - const envFile = args.env as string | undefined; - const sharedFlags = { - apiKey: (args["api-key"] || args.apiKey) as string | undefined, - baseUrl: (args["base-url"] || args.baseUrl) as string | undefined, - json: args.json as boolean, - dryRun: (args["dry-run"] || args.dryRun) as boolean, - }; - - const ctx = resolveContext(sharedFlags); - - const { config } = await loadAgent(agentDir, { envFile }); - const inlineFiles = await collectInlineFiles(agentDir); - - // system_instruction from agent.yaml is sent in the request body. - // AGENTS.md is NOT loaded here — it is uploaded as an inline file and - // merged with the system instruction on the server side. - const systemInstruction = config.instructions; - - // Build environment config - let environment: any; - if (args.environment) { - environment = args.environment; - } else { - const sources: Source[] = [...inlineFiles] as unknown as Source[]; - if (config.sources) { - sources.push(...config.sources); - } - - // Also merge sources from environment.sources (e.g. gcs, github) - const env = config.environment as Record | undefined; - if (env && env.type === "remote" && Array.isArray(env.sources)) { - sources.push(...(env.sources as Source[])); - } - - const normalized = normalizeSources(sources); - validateSources(normalized); - - if (normalized && normalized.length > 0) { - environment = { type: "remote", sources: normalized }; - const envObj = config.environment as any; - if (envObj && typeof envObj === "object" && envObj.network) { - environment.network = envObj.network; - } - } else if (config.environment) { - if (typeof config.environment === "string") { - environment = config.environment; - } else if (typeof config.environment === "object") { - const envObj = config.environment as any; - if (envObj.sources) { - const normalizedEnvSources = normalizeSources(envObj.sources); - validateSources(normalizedEnvSources); - envObj.sources = normalizedEnvSources; - } - environment = envObj; - } - } - } - - // Determine whether base_agent is an agent or model name. - // Known agents use the `agent` field; model names use the `model` field. - const isAgent = isAgentName(config.base_agent); - - const runOpts: RunOptions = { - agent: isAgent ? config.base_agent : undefined, - model: isAgent ? undefined : config.base_agent, - input: prompt, - systemInstruction: systemInstruction, - tools: config.tools as any, - previousInteractionId: args["previous-interaction-id"] as string | undefined, - stream: true, - environment: environment || undefined, - }; - - const body = buildInteractionRequest(runOpts); - - if (args["dry-run"]) { - printCurl("POST", `${ctx.baseUrl}/interactions`, ctx.apiKey, body); - return; - } - - const startTime = performance.now(); - - const response = await apiStreamRequest(ctx, "/interactions", body); - - if (args.json) { - await processStream(response, { - onEvent: (event) => { - console.log(event.raw); - }, - onComplete: () => {}, - }); - } else { - const verbose = args.verbose as boolean; - const renderer = new HumanStreamRenderer(process.stdout, verbose); - - await processStream(response, { - onEvent: (event, block) => { - const mapped = mapContentToStepEvent(event); - renderStepEvent(renderer, mapped, block); - }, - onComplete: (result) => { - renderer.finish(); - const latencySeconds = (performance.now() - startTime) / 1000; - printCompletionSummary(result, latencySeconds, verbose); - if (!args.json) { - logRequest(result.interactionId, body); - logResponse(result.interactionId, result); - } - }, - }); - } - } catch (error) { - if (error instanceof CLIError || error instanceof ConfigError) { - printError(error.message); - } else { - printError(`Unexpected error: ${(error as Error).message}`); - } - process.exit(1); - } - }, -}); diff --git a/src/commands/files/download.ts b/src/commands/files/download.ts deleted file mode 100644 index c797fe9..0000000 --- a/src/commands/files/download.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as fs from "node:fs"; -import * as path from "node:path"; -import { defineCommand } from "citty"; -import { fetchWithTimeout, resolveContext } from "../../lib/api"; -import { CLIError } from "../../lib/errors"; -import { printCurl, printError } from "../../lib/output"; -import { globalFlags } from "../../lib/shared-args"; - -export default defineCommand({ - meta: { - name: "download", - description: `Download files from environment as a snapshot and extract them. - -Examples: - gemini-api files download env_xyz789 - gemini-api files download env_xyz789 --output ./results`, - }, - args: { - ...globalFlags, - "env-id": { - type: "positional", - description: "Environment ID", - required: true, - }, - output: { - type: "string", - description: "Output directory", - default: "./", - }, - }, - async run({ args }) { - try { - const ctx = resolveContext(args); - const envId = args["env-id"]; - const outputDir = args.output || "./"; - - const url = `/files/environment-${envId}:download?alt=media`; - - if (args["dry-run"]) { - printCurl("GET", `${ctx.baseUrl}${url}`, ctx.apiKey); - return; - } - - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - const fullUrl = `${ctx.baseUrl}${url}`; - const headers: Record = { - "x-goog-api-key": ctx.apiKey, - }; - - console.log(`Downloading snapshot for environment ${envId}...`); - const response = await fetchWithTimeout(fullUrl, { headers }); - - if (!response.ok) { - throw new CLIError(`Failed to download snapshot: ${response.statusText}`); - } - - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - const snapshotPath = path.join(outputDir, `snapshot_${envId}.tar`); - fs.writeFileSync(snapshotPath, buffer); - console.log(`✓ Saved snapshot to ${snapshotPath}`); - - // Extract it - const extractDir = path.join(outputDir, `snapshot_${envId}`); - if (!fs.existsSync(extractDir)) { - fs.mkdirSync(extractDir, { recursive: true }); - } - - console.log(`Extracting snapshot to ${extractDir}...`); - try { - const { execSync } = await import("node:child_process"); - execSync(`tar xf ${snapshotPath} -C ${extractDir}`); - console.log(`✓ Extracted snapshot to ${extractDir}`); - fs.unlinkSync(snapshotPath); - } catch (error) { - console.error(`✗ Failed to extract snapshot: ${(error as Error).message}`); - console.log(`Snapshot file is still available at ${snapshotPath}`); - } - } catch (error) { - if (error instanceof CLIError) { - printError(error.message); - } else { - printError(`Unexpected error: ${(error as Error).message}`); - } - process.exit(1); - } - }, -}); diff --git a/src/commands/run.ts b/src/commands/run.ts deleted file mode 100644 index 19c4f5c..0000000 --- a/src/commands/run.ts +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// gemini-api run command - -import { readFileSync } from "node:fs"; -import { defineCommand } from "citty"; -import { - apiGetRequest, - apiRequest, - apiStreamRequest, - buildInteractionRequest, - isDeepResearchAgent, - parseSourceFlag, - parseToolFlag, - type RunOptions, - resolveContext, - type Source, - type Tool, -} from "../lib/api"; -import { inputToContentBlock, saveMediaOutputs } from "../lib/files"; -import { logRequest, logResponse } from "../lib/logger"; -import { - HumanStreamRenderer, - mapContentToStepEvent, - printCompletionSummary, - printCurl, - printError, - printPollingStatus, - renderStepEvent, -} from "../lib/output"; -import { globalFlags } from "../lib/shared-args"; -import type { StreamResult } from "../lib/stream"; -import { processStream } from "../lib/stream"; - -const DEEP_RESEARCH_POLL_INTERVAL_MS = 10_000; - -export default defineCommand({ - meta: { - name: "run", - description: `Create an interaction against a model or agent. - -Examples: - gemini-api run "What is the capital of France?" - gemini-api run "Explain quantum computing" --model gemini-3.1-pro-preview`, - }, - args: { - ...globalFlags, - prompt: { - type: "positional", - description: "Input prompt. Use '-' for stdin.", - required: false, - }, - model: { - type: "string", - alias: "m", - description: "Model to use", - default: "gemini-3.5-flash", - }, - agent: { - type: "string", - alias: "a", - description: "Agent to use (overrides --model)", - }, - "previous-interaction-id": { - type: "string", - alias: "p", - description: "Continue from previous interaction", - }, - "system-instruction": { - type: "string", - alias: "s", - description: "System instruction", - }, - "service-tier": { - type: "string", - description: "Service tier (flex, standard, priority)", - }, - environment: { - type: "string", - description: "Environment to use ('remote' or a specific env_id)", - }, - network: { - type: "string", - description: "Disable network access ('disabled')", - }, - "network-allowlist": { - type: "string", - description: "Comma-separated domain allowlist for network egress", - }, - input: { - type: "string", - alias: "i", - description: "Additional input (can be specified multiple times): :", - }, - tool: { - type: "string", - description: - "Tool declaration (can be specified multiple times): code_execution, google_search, mcp_server:name:url, function:name:schema", - }, - source: { - type: "string", - description: - "Environment source (can be specified multiple times): inline::, github::, gcs::", - }, - "tool-choice": { - type: "string", - description: "Tool choice mode (auto, any, none, validated)", - }, - "response-modality": { - type: "string", - description: "Requested output types (e.g., image, audio)", - }, - "response-mime-type": { - type: "string", - description: "MIME type for response (e.g., application/json)", - }, - output: { - type: "string", - alias: "o", - description: "Save generated media to file", - }, - "aspect-ratio": { - type: "string", - description: "Image aspect ratio (1:1, 16:9, etc.)", - }, - "image-size": { - type: "string", - description: "Image size (512, 1K, 2K, 4K)", - }, - "edit-strength": { - type: "string", - description: "How much to change the original image (0.0 to 1.0)", - }, - mask: { - type: "string", - description: "Path to a mask image for localized editing", - }, - voice: { - type: "string", - description: "TTS voice name", - }, - language: { - type: "string", - description: "Language code for TTS", - }, - }, - async run({ args }) { - let prompt = args.prompt; - if (!prompt && process.argv[process.argv.length - 1] === "-") { - prompt = "-"; - } - if (prompt === "-") { - // Read from stdin - prompt = await new Promise((resolve) => { - let data = ""; - process.stdin.on("data", (chunk) => { - data += chunk; - }); - process.stdin.on("end", () => { - resolve(data); - }); - }); - if (!prompt.trim()) { - console.error("✗ Stdin was empty."); - process.exit(1); - } - } - - if (!prompt) { - printError("Missing prompt.", [ - 'gemini-api run "Your prompt here"', - 'echo "Your prompt" | gemini-api run -', - ]); - process.exit(1); - } - - const sharedFlags = { - apiKey: (args["api-key"] || args.apiKey) as string | undefined, - baseUrl: (args["base-url"] || args.baseUrl) as string | undefined, - json: args.json as boolean, - dryRun: (args["dry-run"] || args.dryRun) as boolean, - }; - - const ctx = resolveContext(sharedFlags); - - // Parse repeated --input flags from process.argv - // (citty doesn't natively support repeated flags well) - const inputs: string[] = []; - for (let i = 0; i < process.argv.length; i++) { - if (process.argv[i] === "--input" || process.argv[i] === "-i") { - if (i + 1 < process.argv.length) { - inputs.push(process.argv[i + 1]); - i++; // Skip the value - } - } - } - - // Parse repeated --tool flags from process.argv - const toolStrings: string[] = []; - for (let i = 0; i < process.argv.length; i++) { - if (process.argv[i] === "--tool") { - if (i + 1 < process.argv.length) { - toolStrings.push(process.argv[i + 1]); - i++; - } - } - } - - // Parse tools - let tools: Tool[] | undefined; - if (toolStrings.length > 0) { - tools = []; - for (const toolStr of toolStrings) { - tools.push(parseToolFlag(toolStr)); - } - } - - // Parse repeated --source flags from process.argv - const sourceStrings: string[] = []; - for (let i = 0; i < process.argv.length; i++) { - if (process.argv[i] === "--source") { - if (i + 1 < process.argv.length) { - sourceStrings.push(process.argv[i + 1]); - i++; - } - } - } - - // Parse sources - let sources: Source[] | undefined; - if (sourceStrings.length > 0) { - sources = []; - for (const sourceStr of sourceStrings) { - sources.push(parseSourceFlag(sourceStr)); - } - } - - let interactionInput: any = prompt; - - if (inputs.length > 0) { - const parts: any[] = [{ type: "text", text: prompt }]; - for (const inputStr of inputs) { - try { - const block = inputToContentBlock(inputStr); - parts.push(block); - } catch (error) { - printError((error as Error).message); - process.exit(1); - } - } - interactionInput = parts; - } - - let maskData: string | undefined; - if (args.mask) { - try { - const data = readFileSync(args.mask as string); - maskData = data.toString("base64"); - } catch (error: any) { - if (error.code === "ENOENT") { - printError(`File not found: ${args.mask}`); - process.exit(1); - } - throw error; - } - } - - let environment: any; - if (args.environment) { - environment = args.environment; - } - - if (args.network || args["network-allowlist"]) { - const envObj: any = { type: "remote" }; - if (args.network === "disabled") { - envObj.network = "disabled"; - } else if (args["network-allowlist"]) { - const domains = (args["network-allowlist"] as string).split(",").map((d) => d.trim()); - envObj.network = { - allowlist: domains.map((domain) => ({ domain })), - }; - } - environment = envObj; - } - - const runOpts: RunOptions = { - model: args.agent ? undefined : (args.model as string | undefined), - agent: args.agent as string | undefined, - input: interactionInput, - systemInstruction: args["system-instruction"] as string | undefined, - tools, - sources, - serviceTier: args["service-tier"] as string | undefined, - previousInteractionId: args["previous-interaction-id"] as string | undefined, - stream: !isDeepResearchAgent(args.agent as string | undefined), - toolChoice: args["tool-choice"] as string | undefined, - - responseModalities: args["response-modality"] - ? [args["response-modality"] as string] - : undefined, - responseMimeType: args["response-mime-type"] as string | undefined, - voice: args.voice as string | undefined, - language: args.language as string | undefined, - aspectRatio: args["aspect-ratio"] as string | undefined, - imageSize: args["image-size"] as string | undefined, - editStrength: args["edit-strength"] ? parseFloat(args["edit-strength"] as string) : undefined, - mask: maskData, - environment, - }; - - const body = buildInteractionRequest(runOpts); - - if (args["dry-run"]) { - printCurl("POST", `${ctx.baseUrl}/interactions`, ctx.apiKey, body); - return; - } - - const startTime = performance.now(); - - // Deep Research agents use streaming with auto-reconnect - if (isDeepResearchAgent(args.agent as string | undefined)) { - await runDeepResearch(ctx, body, args, startTime); - return; - } - - // Standard model/agent streaming - const response = await apiStreamRequest(ctx, "/interactions", body); - - if (args.json) { - await processStream(response, { - onEvent: (event) => { - const data = event.data; - if (data.event_type === "content.delta" && data.delta?.type === "thought_signature") { - return; - } - console.log(event.raw); - }, - onComplete: () => {}, - }); - } else { - const verbose = args.verbose as boolean; - const renderer = new HumanStreamRenderer(process.stdout, verbose); - - await processStream(response, { - onEvent: (event, block) => { - const mapped = mapContentToStepEvent(event); - renderStepEvent(renderer, mapped, block); - }, - onComplete: (result) => { - renderer.finish(); - saveMediaOutputs(result.outputs, result.interactionId, args.output as string | undefined); - const latencySeconds = (performance.now() - startTime) / 1000; - printCompletionSummary(result, latencySeconds, verbose); - if (!args.json) { - logRequest(result.interactionId, body); - logResponse(result.interactionId, result); - } - }, - }); - } - }, -}); - -/** - * Run a Deep Research agent with streaming and automatic reconnection. - * - * Deep Research tasks can take minutes. The SSE connection may drop (e.g., after - * the 30000s server timeout). This function handles: - * 1. Initial POST to start the task (stream=true, background=true) - * 2. Processing stream events as they arrive - * 3. If the connection drops while status is still in_progress, poll the - * interaction status and reconnect the stream using last_event_id - */ -async function runDeepResearch( - ctx: import("../lib/api").CLIContext, - body: object, - args: any, - startTime: number, -): Promise { - let interactionId = ""; - let isComplete = false; - const result: StreamResult = { - status: "in_progress", - outputs: [], - steps: [], - interactionId: "", - }; - - console.error(`⟳ Starting deep research...`); - try { - const response = await apiRequest(ctx, "POST", "/interactions", body); - interactionId = response.id || response.interaction_id; - result.interactionId = interactionId; - console.error(`✓ Deep research started. Interaction ID: ${interactionId}`); - } catch (error) { - printError(`Failed to start deep research: ${(error as Error).message}`); - process.exit(1); - } - - let retryCount = 0; - const maxRetries = 5; - - // 2. Polling loop - while (!isComplete && interactionId) { - const elapsedSeconds = (performance.now() - startTime) / 1000; - printPollingStatus(elapsedSeconds); - - // Wait before polling - await new Promise((resolve) => setTimeout(resolve, DEEP_RESEARCH_POLL_INTERVAL_MS)); - - // Check interaction status via GET - try { - const status = await apiGetRequest(ctx, `/interactions/${interactionId}`); - retryCount = 0; // Reset retry count on success - - if (status.status === "completed" || status.status === "failed") { - isComplete = true; - result.status = status.status; - result.interactionId = interactionId; - - if (status.usage) { - result.usage = { - inputTokens: status.usage.total_input_tokens ?? status.usage.input_tokens, - outputTokens: status.usage.total_output_tokens ?? status.usage.output_tokens, - thoughtTokens: status.usage.total_thought_tokens ?? status.usage.thought_tokens, - }; - } - - // Print final outputs - if (status.outputs && status.outputs.length > 0) { - for (const output of status.outputs) { - if (output.type === "text" && output.text) { - process.stdout.write(output.text); - } - } - } - break; - } - - if (status.status !== "in_progress") { - // Unexpected status - isComplete = true; - break; - } - } catch (_error) { - retryCount++; - const elapsedSeconds = (performance.now() - startTime) / 1000; - console.error( - `⟳ Polling failed, retrying (${retryCount}/${maxRetries})... (${Math.round(elapsedSeconds)}s elapsed)`, - ); - if (retryCount >= maxRetries) { - printError(`Failed to poll status after ${maxRetries} attempts.`); - process.exit(1); - } - // Wait longer on failure - await new Promise((resolve) => setTimeout(resolve, DEEP_RESEARCH_POLL_INTERVAL_MS * 2)); - } - } - - // 3. Finalize output - saveMediaOutputs(result.outputs, result.interactionId, args.output as string | undefined); - const latencySeconds = (performance.now() - startTime) / 1000; - if (!args.json) { - printCompletionSummary(result, latencySeconds); - logRequest(result.interactionId, body); - logResponse(result.interactionId, result); - } -} diff --git a/src/lib/api.ts b/src/lib/api.ts deleted file mode 100644 index a83b4b7..0000000 --- a/src/lib/api.ts +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { CLIError } from "./errors"; - -export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; - -export async function fetchWithTimeout( - url: string, - init?: RequestInit, - timeoutMs = 3000000, -): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - try { - const response = await fetch(url, { - ...init, - signal: controller.signal, - }); - clearTimeout(timeoutId); - return response; - } catch (error) { - clearTimeout(timeoutId); - throw error; - } -} - -export interface CLIContext { - apiKey: string; - baseUrl: string; -} - -export interface SharedFlags { - apiKey?: string; - baseUrl?: string; - json?: boolean; - dryRun?: boolean; - verbose?: boolean; -} - -export function resolveContext(flags: SharedFlags): CLIContext { - const apiKey = flags.apiKey ?? process.env.GEMINI_API_KEY; - if (!apiKey) { - throw new CLIError( - 'No API key found.\n\n Try:\n export GEMINI_API_KEY="your-api-key"\n gemini-api run "Hello" --api-key "your-api-key"', - ); - } - - const baseUrl = flags.baseUrl ?? process.env.GEMINI_API_BASE_URL ?? DEFAULT_BASE_URL; - - return { apiKey, baseUrl }; -} - -function cleanErrorMessage(message: string): string { - return message.replace(/Did you mean '.*?'\?/, "").trim(); -} - -export async function apiRequest( - ctx: CLIContext, - method: string, - path: string, - body?: unknown, -): Promise { - const url = `${ctx.baseUrl}${path}`; - const headers: Record = { - "Content-Type": "application/json", - "x-goog-api-key": ctx.apiKey, - "x-server-timeout": "30000", - }; - - if (path.includes("/interactions")) { - headers["Api-Revision"] = "2026-05-20"; - } - - const response = await fetchWithTimeout(url, { - method, - headers, - body: body ? JSON.stringify(body) : undefined, - }); - - if (!response.ok) { - let errorMsg = `API error (${response.status})`; - try { - const errorData = await response.json(); - if (response.status === 400) { - console.error("400 Error Data:", JSON.stringify(errorData, null, 2)); - } - if (errorData.error?.message) { - errorMsg += `: ${cleanErrorMessage(errorData.error.message)}`; - } - } catch { - // Ignore JSON parse error - } - - if (response.status === 401) { - throw new CLIError(`${errorMsg}\n\n Try:\n export GEMINI_API_KEY="your-api-key"`); - } - - if (response.status === 400) { - throw new CLIError( - `${errorMsg}\n\n Try:\n gemini-api run "Hello" --model gemini-3.5-flash`, - ); - } - - throw new CLIError(errorMsg); - } - - return response.json() as Promise; -} - -export async function apiGetRequest( - ctx: CLIContext, - path: string, - params?: Record, -): Promise { - let url = `${ctx.baseUrl}${path}`; - if (params) { - const qs = new URLSearchParams(params).toString(); - url += `?${qs}`; - } - const headers: Record = { - "x-goog-api-key": ctx.apiKey, - }; - - const response = await fetchWithTimeout(url, { method: "GET", headers }); - - if (!response.ok) { - let errorMsg = `API error (${response.status})`; - try { - const errorData = await response.json(); - if (errorData.error?.message) { - errorMsg += `: ${cleanErrorMessage(errorData.error.message)}`; - } - } catch { - // Ignore JSON parse error - } - throw new CLIError(errorMsg); - } - - return response.json() as Promise; -} - -export async function apiGetStreamRequest( - ctx: CLIContext, - path: string, - params?: Record, -): Promise { - let url = `${ctx.baseUrl}${path}`; - if (params) { - const qs = new URLSearchParams(params).toString(); - url += `?${qs}`; - } - const headers: Record = { - "x-goog-api-key": ctx.apiKey, - }; - - const response = await fetchWithTimeout(url, { method: "GET", headers }); - - if (!response.ok) { - let errorMsg = `API error (${response.status})`; - try { - const errorData = await response.json(); - if (errorData.error?.message) { - errorMsg += `: ${cleanErrorMessage(errorData.error.message)}`; - } - } catch { - // Ignore JSON parse error - } - throw new CLIError(errorMsg); - } - - return response; -} - -export async function apiStreamRequest( - ctx: CLIContext, - path: string, - body: unknown, -): Promise { - const url = `${ctx.baseUrl}${path}`; - const headers: Record = { - "Content-Type": "application/json", - "x-goog-api-key": ctx.apiKey, - "x-server-timeout": "30000", - }; - - if (path.includes("/interactions")) { - headers["Api-Revision"] = "2026-05-20"; - } - - const response = await fetchWithTimeout(url, { - method: "POST", - headers, - body: JSON.stringify(body), - }); - - if (!response.ok) { - let errorMsg = `API error (${response.status})`; - try { - const errorData = await response.json(); - if (response.status === 400) { - console.error("400 Error Data:", JSON.stringify(errorData, null, 2)); - } - if (errorData.error?.message) { - errorMsg += `: ${cleanErrorMessage(errorData.error.message)}`; - } - } catch { - // Ignore JSON parse error - } - throw new CLIError(errorMsg); - } - - return response; -} - -export type InteractionsInput = string | { parts: Array }; - -export interface Tool { - type: string; - [key: string]: unknown; -} - -export function parseToolFlag(value: string): Tool { - // Simple tool types - if ( - [ - "code_execution", - "google_search", - "url_context", - "computer_use", - "file_search", - "google_maps", - "retrieval", - ].includes(value) - ) { - return { type: value }; - } - - // mcp_server:name:url - if (value.startsWith("mcp_server:")) { - const firstColon = value.indexOf(":"); - const secondColon = value.indexOf(":", firstColon + 1); - if (secondColon === -1) { - throw new CLIError("Invalid mcp_server format. Expected: mcp_server:name:url"); - } - const name = value.substring(firstColon + 1, secondColon); - const url = value.substring(secondColon + 1); - if (!name || !url) { - throw new CLIError("Invalid mcp_server format. Expected: mcp_server:name:url"); - } - return { type: "mcp_server", name, url }; - } - - // function:name:schema - if (value.startsWith("function:")) { - const [_, name, ...rest] = value.split(":"); - if (!name || rest.length === 0) { - throw new CLIError("Invalid function format. Expected: function:name:schema"); - } - const parametersStr = rest.join(":"); - try { - const parameters = JSON.parse(parametersStr); - return { type: "function", name, parameters }; - } catch (e) { - throw new CLIError(`Invalid JSON in function schema: ${(e as Error).message}`); - } - } - - throw new CLIError( - `Unknown tool: '${value}'\n\n Available: code_execution, google_search, url_context, computer_use, mcp_server, file_search, google_maps, retrieval, function`, - ); -} - -export interface Source { - type: string; - [key: string]: string; -} - -export function normalizeSource(source: Source): Source { - if (source.type === "github") { - return { ...source, type: "repository" }; - } - return source; -} - -export function normalizeSources(sources?: Source[]): Source[] | undefined { - if (!sources) return undefined; - return sources.map(normalizeSource); -} - -export function validateSources(sources?: Source[]): void { - if (!sources) return; - for (const source of sources) { - if (source.target === "/") { - throw new CLIError('Invalid source target: "/". Custom sources cannot be mounted at root.'); - } - } -} - -export function parseSourceFlag(value: string): Source { - // inline:: - if (value.startsWith("inline:")) { - const rest = value.substring(7); - const idx = rest.indexOf(":"); - if (idx === -1) { - throw new CLIError("Invalid inline source format. Expected: inline::"); - } - return { type: "inline", target: rest.substring(0, idx), content: rest.substring(idx + 1) }; - } - - // github:: — split on last colon since URLs contain colons - if (value.startsWith("github:")) { - const rest = value.substring(7); - const idx = rest.lastIndexOf(":"); - if (idx === -1) { - throw new CLIError("Invalid github source format. Expected: github::"); - } - return { type: "github", source: rest.substring(0, idx), target: rest.substring(idx + 1) }; - } - - // repository:: — split on last colon since URLs contain colons - if (value.startsWith("repository:")) { - const rest = value.substring(11); - const idx = rest.lastIndexOf(":"); - if (idx === -1) { - throw new CLIError("Invalid repository source format. Expected: repository::"); - } - return { type: "repository", source: rest.substring(0, idx), target: rest.substring(idx + 1) }; - } - - // gcs:: — split on last colon - if (value.startsWith("gcs:")) { - const rest = value.substring(4); - const idx = rest.lastIndexOf(":"); - if (idx === -1) { - throw new CLIError("Invalid gcs source format. Expected: gcs::"); - } - return { type: "gcs", source: rest.substring(0, idx), target: rest.substring(idx + 1) }; - } - - throw new CLIError( - `Unknown source type in '${value}'\n\n Available: inline, github, repository, gcs`, - ); -} - -export interface RunOptions { - model?: string; - agent?: string; - input: InteractionsInput; - systemInstruction?: string; - tools?: Tool[]; - responseModalities?: string[]; - responseFormat?: unknown; - responseMimeType?: string; - serviceTier?: string; - previousInteractionId?: string; - stream?: boolean; - - voice?: string; - language?: string; - aspectRatio?: string; - imageSize?: string; - toolChoice?: string; - editStrength?: number; - mask?: string; - - sources?: Source[]; - // Environment override (for agents test) - environment?: string | object; -} - -// Agents that automatically get `environment: { enabled: true }` when used -// via `gemini-api run --agent ` (i.e., without an agent.yaml config). -const _ENVIRONMENT_ENABLED_AGENTS = ["antigravity-preview-05-2026"]; - -// Known agent name prefixes. Everything else is treated as a model name. -const AGENT_PREFIXES = ["antigravity-preview-05-2026", "deep-research"]; - -// Deep Research agent prefixes — these get background:true and agent_config auto-injected. -const DEEP_RESEARCH_PREFIX = "deep-research"; - -/** Returns true if the base_agent value is an agent name (not a model). */ -export function isAgentName(name?: string): boolean { - if (!name) return false; - return AGENT_PREFIXES.some((prefix) => name === prefix || name.startsWith(`${prefix}-`)); -} - -/** Returns true if the agent is a Deep Research agent. */ -export function isDeepResearchAgent(agent?: string): boolean { - if (!agent) return false; - return agent === DEEP_RESEARCH_PREFIX || agent.startsWith(`${DEEP_RESEARCH_PREFIX}-`); -} - -export function buildInteractionRequest(opts: RunOptions): object { - const body: any = { - input: opts.input, - }; - - // Normalize and validate sources - const normalizedSources = normalizeSources(opts.sources); - validateSources(normalizedSources); - - if (normalizedSources && normalizedSources.length > 0) { - body.environment = { type: "remote", sources: normalizedSources }; - } else if (opts.environment) { - if (typeof opts.environment === "string") { - body.environment = opts.environment; - } else if (typeof opts.environment === "object") { - const envObj = opts.environment as any; - if (envObj.sources) { - const envSources = normalizeSources(envObj.sources); - validateSources(envSources); - envObj.sources = envSources; - } - body.environment = envObj; - } - } else if (opts.agent && !isDeepResearchAgent(opts.agent)) { - body.environment = "remote"; - } - - if (opts.model) body.model = opts.model; - if (opts.agent) body.agent = opts.agent; - if (opts.systemInstruction) body.system_instruction = opts.systemInstruction; - if (opts.tools) body.tools = opts.tools; - if (opts.responseModalities) body.response_modalities = opts.responseModalities; - if (opts.responseFormat) body.response_format = opts.responseFormat; - if (opts.responseMimeType) body.response_mime_type = opts.responseMimeType; - if (opts.serviceTier) body.service_tier = opts.serviceTier; - if (opts.previousInteractionId) body.previous_interaction_id = opts.previousInteractionId; - if (opts.stream !== undefined) body.stream = opts.stream; - - // Deep Research agents: auto-inject background:true and agent_config - if (isDeepResearchAgent(opts.agent)) { - body.background = true; - body.agent_config = { - type: "deep-research", - thinking_summaries: "auto", - }; - } - - // Generation Config - const generationConfig: any = {}; - - if (opts.toolChoice) generationConfig.tool_choice = opts.toolChoice; - - if (opts.voice || opts.language) { - const speechConfig: any = {}; - if (opts.voice) speechConfig.voice = opts.voice; - if (opts.language) speechConfig.language = opts.language; - generationConfig.speech_config = [speechConfig]; - } - - if (opts.aspectRatio || opts.imageSize || opts.editStrength !== undefined || opts.mask) { - generationConfig.image_config = {}; - if (opts.aspectRatio) generationConfig.image_config.aspect_ratio = opts.aspectRatio; - if (opts.imageSize) generationConfig.image_config.image_size = opts.imageSize; - if (opts.editStrength !== undefined) - generationConfig.image_config.edit_strength = opts.editStrength; - if (opts.mask) generationConfig.image_config.mask = opts.mask; - } - - if (Object.keys(generationConfig).length > 0) { - body.generation_config = generationConfig; - } - - return body; -} diff --git a/src/lib/config.ts b/src/lib/config.ts deleted file mode 100644 index 76ee151..0000000 --- a/src/lib/config.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { existsSync } from "node:fs"; -import { readFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; -import { parse as parseDotenv } from "dotenv"; -import { ConfigError } from "./errors"; -import { type AgentConfig, AgentConfigSchema } from "./schemas"; -import { parseYaml } from "./yaml"; - -export interface LoadedAgent { - config: AgentConfig; - dir: string; -} - -export interface LoadAgentOptions { - envFile?: string; -} - -async function loadEnvFile(path: string): Promise> { - const envPath = resolve(path); - if (!existsSync(envPath)) { - throw new ConfigError(`Environment file not found: ${path}`); - } - try { - return parseDotenv(await readFile(envPath, "utf-8")); - } catch (error) { - throw new ConfigError(`Failed to read environment file ${path}: ${(error as Error).message}`); - } -} - -function resolveEnvVarString(value: string, envFileVars: Record): string { - const names = new Set( - [...value.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((match) => match[1]), - ); - let resolved = value; - - for (const name of names) { - const envValue = envFileVars[name] ?? process.env[name]; - if (envValue === undefined) { - throw new ConfigError(`Missing environment variable ${name}`); - } - resolved = resolved.replaceAll(`\${${name}}`, () => envValue); - } - - return resolved; -} - -function resolveEnvVars(value: unknown, envFileVars: Record): unknown { - if (typeof value === "string") { - return resolveEnvVarString(value, envFileVars); - } - - if (Array.isArray(value)) { - return value.map((item) => resolveEnvVars(item, envFileVars)); - } - - if (value && typeof value === "object") { - return Object.fromEntries( - Object.entries(value).map(([key, child]) => [key, resolveEnvVars(child, envFileVars)]), - ); - } - - return value; -} - -export async function loadAgent(dir: string, options: LoadAgentOptions = {}): Promise { - const absDir = resolve(dir); - const yamlPath = join(absDir, "agent.yaml"); - - try { - // Read file - const raw = await readFile(yamlPath, "utf-8"); - const envFileVars = options.envFile ? await loadEnvFile(options.envFile) : {}; - const parsed = resolveEnvVars(parseYaml(raw), envFileVars); - - // Validate with Zod - const result = AgentConfigSchema.safeParse(parsed); - if (!result.success) { - const errors = result.error.issues - .map((i) => ` - ${i.path.join(".")}: ${i.message}`) - .join("\n"); - throw new ConfigError(`Invalid agent.yaml:\n${errors}`); - } - - return { config: result.data, dir: absDir }; - } catch (error) { - if (error instanceof ConfigError) { - throw error; - } - throw new ConfigError(`Failed to load agent.yaml: ${(error as Error).message}`); - } -} diff --git a/src/lib/files.ts b/src/lib/files.ts deleted file mode 100644 index 42e2a36..0000000 --- a/src/lib/files.ts +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { readdir, readFile, stat } from "node:fs/promises"; -import { dirname, extname, join, relative } from "node:path"; - -export interface Content { - type: string; - data: string; - mime_type: string; -} - -export function inputToContentBlock(input: string): Content { - const [type, pathOrUrl] = input.split(":", 2); - - if (type === "url") { - return { - type: "url", - data: pathOrUrl, - mime_type: "text/plain", - }; - } - - try { - const data = readFileSync(pathOrUrl); - const base64 = data.toString("base64"); - const mimeType = detectMimeType(pathOrUrl); - - return { - type, - data: base64, - mime_type: mimeType, - }; - } catch (error: any) { - if (error.code === "ENOENT") { - throw new Error(`File not found: ${pathOrUrl}`); - } - throw error; - } -} - -const MIME_TYPES: Record = { - // Images - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".webp": "image/webp", - ".heic": "image/heic", - ".heif": "image/heif", - ".gif": "image/gif", - ".bmp": "image/bmp", - ".tiff": "image/tiff", - - // Audio - ".wav": "audio/wav", - ".mp3": "audio/mp3", - ".aiff": "audio/aiff", - ".aac": "audio/aac", - ".ogg": "audio/ogg", - ".flac": "audio/flac", - ".mpeg": "audio/mpeg", - ".m4a": "audio/m4a", - ".l16": "audio/l16", - ".opus": "audio/opus", - - // Video - ".mp4": "video/mp4", - ".mov": "video/mov", - ".avi": "video/avi", - ".flv": "video/x-flv", - ".webm": "video/webm", - ".wmv": "video/wmv", - ".3gpp": "video/3gpp", - - // Documents - ".pdf": "application/pdf", -}; - -export function detectMimeType(path: string): string { - const ext = extname(path).toLowerCase(); - const mimeType = MIME_TYPES[ext]; - if (!mimeType) { - throw new Error(`Unsupported file extension: ${ext}`); - } - return mimeType; -} - -export function mimeTypeToExt(mimeType: string): string | undefined { - for (const ext in MIME_TYPES) { - if (MIME_TYPES[ext] === mimeType) { - return ext.substring(1); - } - } - return undefined; -} - -function getWavHeader( - pcmLength: number, - sampleRate: number = 24000, - numChannels: number = 1, - bitsPerSample: number = 16, -): Buffer { - const header = Buffer.alloc(44); - - header.write("RIFF", 0); - header.writeUInt32LE(36 + pcmLength, 4); - header.write("WAVE", 8); - - header.write("fmt ", 12); - header.writeUInt32LE(16, 16); - header.writeUInt16LE(1, 20); - header.writeUInt16LE(numChannels, 22); - header.writeUInt32LE(sampleRate, 24); - header.writeUInt32LE(sampleRate * numChannels * (bitsPerSample / 8), 28); - header.writeUInt16LE(numChannels * (bitsPerSample / 8), 32); - header.writeUInt16LE(bitsPerSample, 34); - - header.write("data", 36); - header.writeUInt32LE(pcmLength, 40); - - return header; -} - -export function saveMediaOutputs(outputs: any[], interactionId: string, requestedOutput?: string) { - for (let i = 0; i < outputs.length; i++) { - const block = outputs[i]; - if (["image", "audio", "video", "document"].includes(block.type)) { - const data = block.data; - const mimeType = block.mimeType || block.mime_type; - if (data) { - let filename = requestedOutput; - if (!filename) { - const ext = mimeTypeToExt(mimeType) || "bin"; - filename = `output/${interactionId}_${i}.${ext}`; - } - - const dir = dirname(filename); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - const buffer = Buffer.from(data, "base64"); - if ( - block.type === "audio" && - mimeType === "audio/l16" && - filename.toLowerCase().endsWith(".wav") - ) { - const header = getWavHeader(buffer.length); - writeFileSync(filename, Buffer.concat([header, buffer])); - } else { - writeFileSync(filename, buffer); - } - - console.log(`[${block.type}] Saved to ${filename} (${mimeType})`); - } - } - } -} - -export interface InlineFile { - type: "inline"; - target: string; - content: string; - encoding?: "base64"; -} - -// Extensions that should be read as binary (base64) rather than UTF-8 text -const BINARY_EXTENSIONS = new Set([ - // From MIME_TYPES - ...Object.keys(MIME_TYPES), - // Archives - ".zip", - ".tar", - ".gz", - ".bz2", - ".xz", - ".7z", - // Executables / compiled - ".wasm", - ".so", - ".dylib", - ".dll", - ".exe", - // Other binary - ".bin", - ".dat", - ".db", - ".sqlite", -]); - -function isBinaryFile(filePath: string): boolean { - const ext = extname(filePath).toLowerCase(); - return BINARY_EXTENSIONS.has(ext); -} - -export async function collectInlineFiles( - dir: string, - basePath: string = process.env.AGENTS_WORKSPACE_PATH ?? "/.agents/", -): Promise { - const prefix = basePath.endsWith("/") ? basePath : `${basePath}/`; - const files: InlineFile[] = []; - - async function walk(currentDir: string): Promise { - let entries; - try { - entries = await readdir(currentDir, { withFileTypes: true }); - } catch { - return; - } - - for (const entry of entries) { - const fullPath = join(currentDir, entry.name); - - if (entry.isDirectory()) { - // Only walk into 'workspace' and 'skills' from the root directory - if (currentDir === dir) { - if (entry.name !== "workspace" && entry.name !== "skills") { - continue; - } - } - if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".gemini") - continue; - await walk(fullPath); - continue; - } - - if (entry.isFile()) { - const rel = relative(dir, fullPath); - if (rel === "agent.yaml") continue; - - // If we are in the root directory, only allow AGENTS.md - if (currentDir === dir) { - if (rel !== "AGENTS.md") { - continue; - } - } - - const info = await stat(fullPath); - if (info.size > 1_048_576) continue; - - const target = prefix + rel; - - if (isBinaryFile(fullPath)) { - try { - const buffer = await readFile(fullPath); - const content = buffer.toString("base64"); - files.push({ type: "inline", target, content, encoding: "base64" }); - } catch { - // Skip unreadable binary files - } - } else { - try { - const content = await readFile(fullPath, "utf-8"); - files.push({ type: "inline", target, content }); - } catch { - // Skip unreadable text files - } - } - } - } - } - - await walk(dir); - return files; -} diff --git a/src/lib/logger.ts b/src/lib/logger.ts deleted file mode 100644 index 91ec676..0000000 --- a/src/lib/logger.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { appendFileSync, mkdirSync } from "node:fs"; -import { join } from "node:path"; -import type { ContentBlock, StreamResult } from "./stream"; - -const LOG_DIR = join(process.cwd(), ".gemini", "logs"); - -export function logRequest(interactionId: string, request: object): void { - try { - mkdirSync(LOG_DIR, { recursive: true }); - const line = JSON.stringify({ - type: "request", - timestamp: new Date().toISOString(), - data: request, - }); - appendFileSync(join(LOG_DIR, `${interactionId}.jsonl`), `${line}\n`); - } catch (e) { - console.error(`Failed to log request: ${(e as Error).message}`); - } -} - -export function logResponse(interactionId: string, result: StreamResult): void { - try { - mkdirSync(LOG_DIR, { recursive: true }); - - // Strip binary data from outputs - const outputs = result.outputs.map(stripBinaryData); - - const line = JSON.stringify({ - type: "response", - timestamp: new Date().toISOString(), - data: { - id: result.interactionId, - status: result.status, - outputs, - usage: result.usage, - created: result.created, - updated: result.updated, - }, - }); - appendFileSync(join(LOG_DIR, `${interactionId}.jsonl`), `${line}\n`); - } catch (e) { - console.error(`Failed to log response: ${(e as Error).message}`); - } -} - -function stripBinaryData(block: ContentBlock): ContentBlock { - if (["image", "audio", "document", "video"].includes(block.type)) { - const stripped = { ...block }; - delete (stripped as any).data; - return stripped as ContentBlock; - } - return block; -} diff --git a/src/lib/output.ts b/src/lib/output.ts deleted file mode 100644 index bc81337..0000000 --- a/src/lib/output.ts +++ /dev/null @@ -1,607 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import type { ContentBlock, StreamEvent, StreamResult } from "./stream"; - -export function printCurl(method: string, url: string, apiKey: string, body?: unknown): void { - let curl = `curl -X ${method} "${url}" \\\n`; - curl += ` -H "Content-Type: application/json" \\\n`; - curl += ` -H "x-goog-api-key: ${apiKey}" \\\n`; - curl += ` -H "x-server-timeout: 30000"`; - - if (url.includes("/interactions")) { - curl += ` \\\n -H "Api-Revision: 2026-05-20"`; - } - - if (body) { - // Escape single quotes in body for bash - const bodyStr = JSON.stringify(body, null, 2).replace(/'/g, "'\\''"); - curl += ` \\\n -d '${bodyStr}'`; - } - - console.log(curl); -} - -export class HumanStreamRenderer { - private currentStepIndex: number | null = null; - private currentStepType: string | null = null; - private currentStepName: string | null = null; - private prefixPrinted = false; - private accumulatedArguments = ""; - private accumulatedResult = ""; - private colWidth = 15; - - // Verbose state accumulation - private currentStepThought: any = null; - private currentStepFunctionCall: any = null; - private currentStepFunctionResult: any = null; - private currentStepCodeCall: any = null; - private currentStepCodeResult: any = null; - private currentStepModelOutput: any = null; - - // Normal mode buffering (to combine tool call + result) - private bufferedToolCall: string | null = null; - private bufferedToolType: string | null = null; - - constructor( - private stdout: typeof process.stdout = process.stdout, - private verbose = false, - ) {} - - private getPrefix(type: string): string { - const prefixes: Record = { - text: "[text]", - thought: "[thought]", - thought_summary: "[thought]", - function_call: "[tool]", - function_result: "[result]", - code_execution_call: "[code]", - code_execution_result: "[result]", - google_search_call: "[search]", - google_search_result: "[grounding]", - url_context_call: "[url]", - url_context_result: "[url-result]", - mcp_server_tool_call: "[mcp]", - mcp_server_tool_result: "[mcp-result]", - file_search_call: "[file-search]", - file_search_result: "[file-result]", - google_maps_call: "[maps]", - google_maps_result: "[maps-result]", - image: "[image]", - audio: "[audio]", - video: "[video]", - document: "[document]", - }; - return type in prefixes ? prefixes[type] : `[${type}]`; - } - - private codeExecutionIsError = false; - - handleStepStart(event: StreamEvent, block?: ContentBlock) { - const index = event.data.index ?? event.data.step_index; - if (index === undefined) return; - - // Finalize previous step if index changed - if (this.currentStepIndex !== null && this.currentStepIndex !== index) { - this.finalizeStep(); - } - - this.currentStepIndex = index; - this.currentStepType = event.data.step?.type || "unknown"; - this.currentStepName = event.data.step?.name || null; - this.prefixPrinted = false; - this.accumulatedArguments = ""; - this.accumulatedResult = ""; - - if (this.verbose) { - this.currentStepThought = null; - this.currentStepFunctionCall = null; - this.currentStepFunctionResult = null; - this.currentStepCodeCall = null; - this.currentStepCodeResult = null; - this.currentStepModelOutput = null; - return; - } - - if (this.currentStepType === "thought") { - const prefix = this.getPrefix("thought"); - this.stdout.write(`${prefix}\n`); - this.prefixPrinted = true; - } else if (this.currentStepType === "model_output") { - let hasText = false; - if (Array.isArray(event.data.step?.content)) { - hasText = event.data.step.content.some((c: any) => c.type === "text" && c.text); - } - if (hasText) { - const prefix = this.getPrefix("text"); - this.stdout.write(`${prefix}\n`); - this.prefixPrinted = true; - for (const c of event.data.step.content) { - if (c.type === "text" && c.text) { - this.stdout.write(c.text); - } - } - } - } - } - - handleStepDelta(event: StreamEvent, block?: ContentBlock) { - const index = event.data.index ?? event.data.step_index; - if (index === undefined || this.currentStepIndex !== index) return; - - const delta = event.data.delta; - if (!delta) return; - - if ((!this.currentStepType || this.currentStepType === "unknown") && block?.type) { - this.currentStepType = block.type; - } - const type = this.currentStepType || "text"; - - if (this.verbose) { - if (type === "thought" || type === "thought_summary" || type === "thought_signature") { - if (!this.currentStepThought) this.currentStepThought = {}; - if (delta.signature) this.currentStepThought.signature = delta.signature; - if (delta.text) - this.currentStepThought.text = (this.currentStepThought.text || "") + delta.text; - } else if (type === "function_call") { - if (!this.currentStepFunctionCall) - this.currentStepFunctionCall = { name: "", arguments: "" }; - if (delta.name) this.currentStepFunctionCall.name = delta.name; - if (delta.id) this.currentStepFunctionCall.id = delta.id; - if (delta.arguments) { - if (typeof delta.arguments === "string") { - this.currentStepFunctionCall.arguments += delta.arguments; - } else { - this.currentStepFunctionCall.arguments = delta.arguments; - } - } - } else if (type === "function_result") { - if (!this.currentStepFunctionResult) this.currentStepFunctionResult = { result: "" }; - if (delta.name) this.currentStepFunctionResult.name = delta.name; - if (delta.call_id) this.currentStepFunctionResult.call_id = delta.call_id; - if (delta.result) { - if (typeof delta.result === "string") { - this.currentStepFunctionResult.result += delta.result; - } else { - this.currentStepFunctionResult.result = delta.result; - } - } - } else if (type === "code_execution_call") { - if (!this.currentStepCodeCall) this.currentStepCodeCall = { code: "" }; - if (delta.id) this.currentStepCodeCall.id = delta.id; - if (delta.arguments) { - if (delta.arguments.language) - this.currentStepCodeCall.language = delta.arguments.language; - if (delta.arguments.code) this.currentStepCodeCall.code += delta.arguments.code; - } - } else if (type === "code_execution_result") { - if (!this.currentStepCodeResult) - this.currentStepCodeResult = { result: "", is_error: false }; - if (delta.call_id) this.currentStepCodeResult.call_id = delta.call_id; - if (delta.is_error !== undefined) this.currentStepCodeResult.is_error = delta.is_error; - if (delta.result) this.currentStepCodeResult.result += delta.result; - } else if (type === "model_output" || type === "text") { - if (!this.currentStepModelOutput) this.currentStepModelOutput = { content: [] }; - if (delta.text) { - let textPart = this.currentStepModelOutput.content.find((c: any) => c.type === "text"); - if (!textPart) { - textPart = { type: "text", text: "" }; - this.currentStepModelOutput.content.push(textPart); - } - textPart.text += delta.text; - } - if (delta.data) { - let mediaPart = this.currentStepModelOutput.content.find((c: any) => c.type !== "text"); - if (!mediaPart) { - mediaPart = { type: delta.type || "image", data: "" }; - this.currentStepModelOutput.content.push(mediaPart); - } - mediaPart.data += delta.data; - if (delta.mime_type) mediaPart.mimeType = delta.mime_type; - } - } - return; - } - - if (type === "thought" || type === "thought_summary" || type === "thought_signature") { - return; - } - - if (type === "function_call" || type === "code_execution_call") { - if (delta.arguments) { - // Assume delta.arguments is streamed as string chunks containing JSON fragments. - // If it is pre-parsed or delivered as objects, concatenation will result in invalid JSON. - this.accumulatedArguments += - typeof delta.arguments === "string" ? delta.arguments : JSON.stringify(delta.arguments); - } - if (delta.name) { - this.currentStepName = delta.name; - } - return; - } - - if (type === "function_result") { - if (delta.result) { - this.accumulatedResult += - typeof delta.result === "string" ? delta.result : JSON.stringify(delta.result); - } - if (delta.name) { - this.currentStepName = delta.name; - } - if (delta.call_id) { - // Just in case - } - return; - } - - if (type === "code_execution_result") { - if (delta.result) this.accumulatedResult += delta.result; - if (delta.is_error !== undefined) this.codeExecutionIsError = delta.is_error; - return; - } - - if ((type === "model_output" || type === "text") && delta.text) { - if (!this.prefixPrinted) { - const prefix = this.getPrefix("text"); - this.stdout.write(`${prefix}\n`); - this.prefixPrinted = true; - } - this.stdout.write(delta.text); - } - } - - handleStepStop(event: StreamEvent, block?: ContentBlock) { - const index = event.data.index ?? event.data.step_index; - if (index === undefined || this.currentStepIndex !== index) return; - - this.finalizeStep(); - } - - private finalizeStep() { - if (this.currentStepIndex === null) return; - - const type = this.currentStepType; - const name = this.currentStepName; - - if (this.verbose) { - const stepObj: any = { - index: this.currentStepIndex, - type: type || "unknown", - status: "completed", - }; - - if (type === "thought" && this.currentStepThought) { - stepObj.thought = this.currentStepThought; - } else if (type === "function_call") { - let args = this.currentStepFunctionCall?.arguments || this.accumulatedArguments; - if (typeof args === "string" && args.trim()) { - try { - args = JSON.parse(args); - } catch {} - } - stepObj.function_call = { - name: this.currentStepFunctionCall?.name || name || "unknown", - arguments: args, - id: this.currentStepFunctionCall?.id, - }; - } else if (type === "function_result") { - let res = this.currentStepFunctionResult?.result || this.accumulatedResult; - if (typeof res === "string" && res.trim()) { - try { - res = JSON.parse(res); - } catch {} - } - stepObj.function_result = { - name: this.currentStepFunctionResult?.name || name, - result: res, - call_id: this.currentStepFunctionResult?.call_id, - }; - } else if (type === "code_execution_call") { - let code = this.currentStepCodeCall?.code || ""; - if (!code && this.accumulatedArguments) { - try { - const parsed = JSON.parse(this.accumulatedArguments); - code = parsed.code || parsed.raw || ""; - } catch { - code = this.accumulatedArguments; - } - } - stepObj.code_execution_call = { - language: this.currentStepCodeCall?.language || "python", - code, - id: this.currentStepCodeCall?.id, - }; - } else if (type === "code_execution_result") { - stepObj.code_execution_result = { - result: this.currentStepCodeResult?.result || this.accumulatedResult, - is_error: this.currentStepCodeResult?.is_error || false, - call_id: this.currentStepCodeResult?.call_id, - }; - } else if ((type === "model_output" || type === "text") && this.currentStepModelOutput) { - stepObj.model_output = this.currentStepModelOutput; - } - - this.stdout.write(JSON.stringify(stepObj) + "\n"); - - // Reset verbose states - this.currentStepThought = null; - this.currentStepFunctionCall = null; - this.currentStepFunctionResult = null; - this.currentStepCodeCall = null; - this.currentStepCodeResult = null; - this.currentStepModelOutput = null; - - this.currentStepIndex = null; - this.currentStepType = null; - this.currentStepName = null; - this.accumulatedArguments = ""; - this.accumulatedResult = ""; - return; - } - - if (type === "function_call") { - let argsObj: any = {}; - try { - argsObj = JSON.parse(this.accumulatedArguments); - } catch { - argsObj = { raw: this.accumulatedArguments }; - } - - if (name === "write_file") { - const path = argsObj.path || "unknown"; - this.bufferedToolCall = `write_file(path="${path}")`; - } else { - const argsStr = JSON.stringify(argsObj); - const truncatedArgs = argsStr.length > 100 ? argsStr.substring(0, 100) + "..." : argsStr; - this.bufferedToolCall = `${name || "unknown"}(${truncatedArgs})`; - } - this.bufferedToolType = "function_call"; - } else if (type === "code_execution_call") { - let argsObj: any = {}; - try { - argsObj = JSON.parse(this.accumulatedArguments); - } catch { - argsObj = { code: this.accumulatedArguments }; - } - const code = argsObj.code || argsObj.raw || ""; - const cleanCode = code.trim().replace(/\n/g, "; "); - this.bufferedToolCall = cleanCode; - this.bufferedToolType = "code_execution_call"; - } else if (type === "function_result") { - const prefix = this.getPrefix("function_call"); - let resultObj: any = {}; - try { - resultObj = JSON.parse(this.accumulatedResult); - } catch { - resultObj = this.accumulatedResult; - } - - let resultStr = ""; - if (resultObj && typeof resultObj === "object") { - if (resultObj.error) { - resultStr = `Error: ${resultObj.error}`; - } else { - resultStr = JSON.stringify(resultObj); - } - } else { - resultStr = String(resultObj); - } - - this.stdout.write(`${prefix} ${this.bufferedToolCall || "unknown()"} -> ${resultStr}\n`); - this.bufferedToolCall = null; - this.bufferedToolType = null; - } else if (type === "code_execution_result") { - const prefix = this.getPrefix("code_execution_call"); - const resultStr = this.accumulatedResult.trim(); - - let displayResult = ""; - if (this.codeExecutionIsError) { - displayResult = `Error: ${resultStr}`; - } else { - displayResult = resultStr ? `"${resultStr.replace(/\n/g, "\\n")}"` : "success"; - } - - this.stdout.write(`${prefix} ${this.bufferedToolCall || "code"} -> ${displayResult}\n`); - this.bufferedToolCall = null; - this.bufferedToolType = null; - this.codeExecutionIsError = false; - } - - this.currentStepIndex = null; - this.currentStepType = null; - this.currentStepName = null; - this.prefixPrinted = false; - this.accumulatedArguments = ""; - this.accumulatedResult = ""; - } - - finish() { - this.finalizeStep(); - } -} - -/** - * Maps content.* SSE events to step.* events for the renderer. - * Some API endpoints still emit content.start/delta/stop; this function - * normalises them so the renderer only needs to handle step.* events. - */ -export function mapContentToStepEvent(event: StreamEvent): StreamEvent { - if (event.type === "content.start") { - return { - type: "step.start", - data: { - index: event.data.index, - step: { type: event.data.content?.type || "text", status: "in_progress" }, - }, - raw: event.raw, - }; - } - if (event.type === "content.delta") { - return { - type: "step.delta", - data: { - index: event.data.index, - delta: event.data.delta, - }, - raw: event.raw, - }; - } - if (event.type === "content.stop") { - return { - type: "step.stop", - data: { - index: event.data.index, - }, - raw: event.raw, - }; - } - return event; -} - -/** Dispatch a (possibly mapped) step event to the renderer. */ -export function renderStepEvent( - renderer: HumanStreamRenderer, - event: StreamEvent, - block?: ContentBlock, -): void { - if (event.type === "step.start") { - renderer.handleStepStart(event, block); - } else if (event.type === "step.delta") { - renderer.handleStepDelta(event, block); - } else if (event.type === "step.stop") { - renderer.handleStepStop(event, block); - } -} - -export function printCompletionSummary( - result: StreamResult, - latencySeconds: number, - verbose = false, -): void { - if (verbose) { - const summaryObj: any = { - interaction: { - id: result.interactionId, - status: result.status || "completed", - }, - }; - if (result.environmentId) { - summaryObj.interaction.environment_id = result.environmentId; - } - if (result.usage) { - const inTokens = result.usage.inputTokens || 0; - const outTokens = result.usage.outputTokens || 0; - summaryObj.interaction.usage = { - total_tokens: inTokens + outTokens, - total_input_tokens: inTokens, - total_output_tokens: outTokens, - total_thought_tokens: result.usage.thoughtTokens || 0, - total_cached_tokens: result.usage.cachedTokens || 0, - }; - } - if (result.created) summaryObj.interaction.created = result.created; - if (result.updated) summaryObj.interaction.updated = result.updated; - summaryObj.interaction.object = "interaction"; - - console.log(JSON.stringify(summaryObj)); - } else { - console.log("\n✓ completed"); - console.log(` interaction_id: ${result.interactionId}`); - - if (result.environmentId) { - console.log(` environment_id: ${result.environmentId}`); - } - - if (result.usage) { - const inTokens = result.usage.inputTokens?.toLocaleString() ?? "0"; - const outTokens = result.usage.outputTokens?.toLocaleString() ?? "0"; - const thoughtTokens = result.usage.thoughtTokens?.toLocaleString() ?? "0"; - const cachedTokens = result.usage.cachedTokens !== undefined ? ` cached:${result.usage.cachedTokens.toLocaleString()}` : ""; - console.log(` tokens: in:${inTokens} out:${outTokens} thought:${thoughtTokens}${cachedTokens}`); - } - console.log(` latency: ${latencySeconds.toFixed(1)}s`); - } -} - -export function printError(message: string, tryCommands?: string[]): void { - console.error(`✗ ${message}\n`); - if (tryCommands && tryCommands.length > 0) { - console.error(" Try:"); - for (const cmd of tryCommands) { - console.error(` ${cmd}`); - } - } -} - -export function printBlock(block: ContentBlock): void { - if (block.type === "text") return; - - const prefixes: Record = { - function_call: "[tool]", - function_result: "[result]", - code_execution_call: "[code]", - code_execution_result: "[result]", - google_search_call: "[search]", - google_search_result: "[grounding]", - url_context_call: "[url]", - url_context_result: "[url-result]", - mcp_server_tool_call: "[mcp]", - mcp_server_tool_result: "[mcp-result]", - file_search_call: "[file-search]", - file_search_result: "[file-result]", - google_maps_call: "[maps]", - google_maps_result: "[maps-result]", - image: "[image]", - audio: "[audio]", - video: "[video]", - document: "[document]", - }; - - const prefix = prefixes[block.type] || `[${block.type}]`; - const colWidth = 15; - const prefixStr = prefix.padEnd(colWidth); - - switch (block.type) { - case "function_call": - console.log(`${prefixStr}${block.name}(${JSON.stringify(block.arguments)})`); - break; - case "function_result": - console.log( - `${prefixStr}${typeof block.result === "string" ? block.result : JSON.stringify(block.result)}`, - ); - break; - case "code_execution_call": - console.log(`${prefixStr}\`\`\`\n${block.arguments?.code || ""}\n\`\`\``); - break; - case "code_execution_result": - console.log(`${prefixStr}${block.result}`); - break; - case "google_search_call": - console.log(`${prefixStr}query: ${block.query}`); - break; - default: { - const blockStr = JSON.stringify(block); - if (blockStr.length < 100) { - console.log(`${prefixStr}${blockStr}`); - } else { - console.log(`${prefixStr}${block.type} completed`); - } - break; - } - } -} - -export function printPollingStatus(elapsedSeconds: number, status: string = "in_progress"): void { - console.error(`\r⟳ deep-research ${status}... (${Math.round(elapsedSeconds)}s elapsed)`); -} diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts deleted file mode 100644 index 2172406..0000000 --- a/src/lib/schemas.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { z } from "zod"; - -export const ToolSchema = z - .object({ - type: z.enum(["code_execution", "google_search", "url_context"]), - }) - .passthrough(); - -const SourceSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("gcs"), - source: z.string(), - target: z.string(), - }), - z.object({ - type: z.literal("inline"), - content: z.string(), - target: z.string(), - }), - z.object({ - type: z.literal("github"), - source: z.string(), - target: z.string(), - }), - z.object({ - type: z.literal("repository"), - source: z.string(), - target: z.string(), - }), -]); - -const NetworkRuleSchema = z.object({ - domain: z.string(), - transform: z.record(z.string()).optional(), -}); - -const NetworkConfigSchema = z.union([ - z.literal("disabled"), - z.object({ - allowlist: z.array(NetworkRuleSchema), - }), -]); - -const RemoteEnvironmentSchema = z.object({ - type: z.literal("remote"), - sources: z.array(SourceSchema).optional(), - network: NetworkConfigSchema.optional(), -}); - -export const EnvironmentSchema = z.union([ - z.string(), // Supports "remote" or "env_xyz" - RemoteEnvironmentSchema, -]); - -const ExampleSchema = z.object({ - title: z.string(), - prompt: z.string(), -}); - -export const AgentConfigSchema = z - .object({ - id: z.string(), - base_agent: z.literal("antigravity-preview-05-2026").optional(), - description: z.string().optional(), - instructions: z.string().optional(), - tools: z.array(ToolSchema).optional(), - base_environment: z.union([z.string(), RemoteEnvironmentSchema]).optional(), - sources: z.array(SourceSchema).optional(), - environment: EnvironmentSchema.optional(), - examples: z.array(ExampleSchema).optional(), - }) - .strict(); - -export type AgentConfig = z.infer; - -export const CLIContextSchema = z.object({ - apiKey: z.string(), - baseUrl: z.string().url(), -}); - -export type CLIContext = z.infer; diff --git a/src/lib/shared-args.ts b/src/lib/shared-args.ts deleted file mode 100644 index 96c6f10..0000000 --- a/src/lib/shared-args.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -export const globalFlags = { - "api-key": { - type: "string" as const, - description: "API key for authentication (or set GEMINI_API_KEY)", - }, - "base-url": { - type: "string" as const, - description: "Override API base URL (or set GEMINI_API_BASE_URL)", - }, - json: { - type: "boolean" as const, - description: "JSON output mode", - default: false, - }, - "dry-run": { - type: "boolean" as const, - description: "Print curl and exit", - default: false, - }, - verbose: { - type: "boolean" as const, - alias: "v", - description: "Verbose output (JSON lines per step, full details)", - default: false, - }, -}; diff --git a/src/lib/stream.ts b/src/lib/stream.ts deleted file mode 100644 index 15e94f9..0000000 --- a/src/lib/stream.ts +++ /dev/null @@ -1,469 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { APIError, CLIError } from "./errors"; - -export interface StreamEvent { - type: - | "interaction.created" - | "content.start" - | "content.delta" - | "content.stop" - | "step.start" - | "step.delta" - | "step.stop" - | "interaction.completed" - | "interaction.status_update" - | "error"; - data: any; - raw: string; // Original SSE JSON for --json mode -} - -export interface Usage { - inputTokens?: number; - outputTokens?: number; - thoughtTokens?: number; - cachedTokens?: number; -} - -export interface StepInfo { - index: number; - type?: string; - status?: string; - text?: string; -} - -export interface StreamResult { - interactionId: string; - status: string; - outputs: ContentBlock[]; // Reassembled content blocks - steps: StepInfo[]; // Accumulated step data - usage?: Usage; - created?: string; - updated?: string; - environmentId?: string; - lastEventId?: string; // For deep-research stream reconnection -} - -export type ContentBlock = - | { type: "text"; text: string } - | { type: "image"; data: string; mimeType: string } - | { type: "audio"; data: string; mimeType: string } - | { type: "document"; data: string; mimeType: string } - | { type: "video"; data: string; mimeType: string } - | { type: "function_call"; name: string; arguments: object; id: string } - | { type: "code_execution_call"; arguments: { code: string }; id: string } - | { type: "code_execution_result"; result: string; isError: boolean; callId: string } - | { type: "thought_summary"; text: string } - | { type: "thought_signature"; signature: string } - | { type: "url_context_call"; url: string } - | { type: "google_search_call"; query: string } - | { type: "mcp_server_tool_call"; server: string; tool: string; arguments: object } - | { type: "file_search_call"; query: string } - | { type: "google_maps_call"; query: string } - | { type: "function_result"; result: unknown; callId: string } - | { type: "url_context_result"; result: unknown; callId: string } - | { type: "google_search_result"; result: unknown; callId: string } - | { type: "mcp_server_tool_result"; result: unknown; callId: string } - | { type: "file_search_result"; result: unknown; callId: string } - | { type: "google_maps_result"; result: unknown; callId: string } - | { type: "text_annotation"; annotations: unknown[] }; - -export async function processStream( - response: Response, - callbacks: { - onEvent: (event: StreamEvent, block?: ContentBlock) => void; // Called for each SSE event - onComplete: (result: StreamResult) => void; // Called when stream ends - onBlockComplete?: (block: ContentBlock) => void; // Called when a block is complete - }, -): Promise { - if (!response.body) { - throw new CLIError("Response body is null"); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - - const result: StreamResult = { - interactionId: "", - status: "", - outputs: [], - steps: [], - }; - - const contentBlocks: Map = new Map(); - const completedBlocks = new Set(); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - - if (trimmed.startsWith("data:")) { - const dataStr = trimmed.substring(5).trim(); - if (dataStr === "[DONE]") continue; - - try { - const data = JSON.parse(dataStr); - const event: StreamEvent = { - type: data.event_type, - data: data, - raw: dataStr, - }; - handleEvent(event, result, contentBlocks); - const block = - event.data && event.data.index !== undefined - ? contentBlocks.get(event.data.index) - : undefined; - callbacks.onEvent(event, block); - - if (event.type === "content.stop") { - const index = event.data.index; - completedBlocks.add(index); - const block = contentBlocks.get(index); - if (block) { - callbacks.onBlockComplete?.(block); - } - } - } catch (_e) { - // Malformed SSE lines are handled gracefully - // console.warn("Failed to parse SSE data:", dataStr, e); - } - } - } - } - - // Handle remaining buffer - if (buffer.trim().startsWith("data:")) { - const dataStr = buffer.trim().substring(5).trim(); - if (dataStr !== "[DONE]") { - try { - const data = JSON.parse(dataStr); - const event: StreamEvent = { - type: data.event_type, - data: data, - raw: dataStr, - }; - callbacks.onEvent(event); - handleEvent(event, result, contentBlocks); - } catch (_e) { - // Ignore - } - } - } - } catch (e) { - throw new APIError(`SSE connection error: ${(e as Error).message}`); - } - - // Call onBlockComplete for any blocks that didn't get content.stop - for (const [index, block] of contentBlocks.entries()) { - if (!completedBlocks.has(index)) { - callbacks.onBlockComplete?.(block); - } - } - - // Convert contentBlocks map to array - result.outputs = Array.from(contentBlocks.values()); - - // Remove holes from sparse steps array - result.steps = result.steps.filter(Boolean); - - // Finalize any blocks if needed - for (const block of result.outputs) { - if (block.type === "function_call" && typeof (block as any).arguments === "string") { - try { - (block as any).arguments = JSON.parse((block as any).arguments); - } catch { - // Ignore - } - } - if (block.type === "mcp_server_tool_call" && typeof (block as any).arguments === "string") { - try { - (block as any).arguments = JSON.parse((block as any).arguments); - } catch { - // Ignore - } - } - } - - callbacks.onComplete(result); - return result; -} - -function handleEvent( - event: StreamEvent, - result: StreamResult, - contentBlocks: Map, -) { - const data = event.data; - - if (data.interaction) { - if (data.interaction.id) result.interactionId = data.interaction.id; - if (data.interaction.status) result.status = data.interaction.status; - if (data.interaction.created_at) result.created = data.interaction.created_at; - if (data.interaction.updated_at) result.updated = data.interaction.updated_at; - if (data.interaction.environment_id) result.environmentId = data.interaction.environment_id; - } - if (data.interaction_id) { - result.interactionId = data.interaction_id; - } - if (data.environment_id) { - result.environmentId = data.environment_id; - } - if (data.event_id) { - result.lastEventId = data.event_id; - } - - if (event.type === "content.start") { - const index = data.index; - const content = data.content; - if (content?.type) { - const block: any = { type: content.type }; - if (content.name) block.name = content.name; - contentBlocks.set(index, block); - } - } else if (event.type === "content.delta") { - const index = data.index; - const delta = data.delta; - const block = contentBlocks.get(index); - - if (block && delta) { - switch (block.type) { - case "text": - if (delta.text) (block as any).text = ((block as any).text || "") + delta.text; - break; - case "image": - case "audio": - case "document": - case "video": - if (delta.data) (block as any).data = ((block as any).data || "") + delta.data; - if (delta.mime_type) (block as any).mimeType = delta.mime_type; - break; - case "function_call": - if (delta.name) (block as any).name = delta.name; - if (delta.arguments) { - if (typeof delta.arguments === "string") { - (block as any).arguments = ((block as any).arguments || "") + delta.arguments; - } else { - (block as any).arguments = delta.arguments; - } - } - if (delta.id) (block as any).id = delta.id; - break; - case "code_execution_call": - if (delta.arguments?.code) { - (block as any).arguments = (block as any).arguments || { code: "" }; - (block as any).arguments.code += delta.arguments.code; - } - if (delta.id) (block as any).id = delta.id; - break; - case "code_execution_result": - if (delta.result) (block as any).result = ((block as any).result || "") + delta.result; - if (delta.is_error !== undefined) (block as any).isError = delta.is_error; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "thought_summary": - if (delta.text) (block as any).text = ((block as any).text || "") + delta.text; - break; - case "thought_signature": - if (delta.signature) (block as any).signature = delta.signature; - break; - case "url_context_call": - if (delta.url) (block as any).url = delta.url; - break; - case "google_search_call": - if (delta.query) (block as any).query = delta.query; - break; - case "mcp_server_tool_call": - if (delta.server) (block as any).server = delta.server; - if (delta.tool) (block as any).tool = delta.tool; - if (delta.arguments) { - if (typeof delta.arguments === "string") { - (block as any).arguments = ((block as any).arguments || "") + delta.arguments; - } else { - (block as any).arguments = delta.arguments; - } - } - break; - case "file_search_call": - if (delta.query) (block as any).query = delta.query; - break; - case "google_maps_call": - if (delta.query) (block as any).query = delta.query; - break; - case "function_result": - if (delta.result) (block as any).result = delta.result; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "url_context_result": - if (delta.result) (block as any).result = delta.result; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "google_search_result": - if (delta.result) (block as any).result = delta.result; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "mcp_server_tool_result": - if (delta.result) (block as any).result = delta.result; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "file_search_result": - if (delta.result) (block as any).result = delta.result; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "google_maps_result": - if (delta.result) (block as any).result = delta.result; - if (delta.call_id) (block as any).callId = delta.call_id; - break; - case "text_annotation": - if (delta.annotations) { - (block as any).annotations = (block as any).annotations || []; - (block as any).annotations.push(...delta.annotations); - } - break; - } - } - } else if (event.type === "step.start") { - const index = data.index ?? data.step_index; - if (index !== undefined) { - const step: StepInfo = { index }; - if (data.step?.type) step.type = data.step.type; - if (data.step?.status) step.status = data.step.status; - result.steps[index] = step; - if (data.step?.type === "model_output" && Array.isArray(data.step.content)) { - for (const c of data.step.content) { - if (["text", "image", "audio", "video", "document"].includes(c.type)) { - const block: any = { type: c.type }; - if (c.text) block.text = c.text; - if (c.data) block.data = c.data; - if (c.mime_type) block.mimeType = c.mime_type; - contentBlocks.set(index, block); - } - } - } - } - } else if (event.type === "step.delta") { - const index = data.index ?? data.step_index; - if (index !== undefined) { - const step = result.steps[index] || { index }; - const delta = data.delta; - if (delta) { - if (delta.text) step.text = (step.text || "") + delta.text; - if (delta.type) step.type = delta.type; - if (delta.status) step.status = delta.status; - - // Also append media/block data to contentBlocks if available - let block = contentBlocks.get(index); - if (!block && delta.type) { - let type: string | undefined; - if ( - [ - "text", - "image", - "audio", - "video", - "document", - "function_call", - "code_execution_call", - "code_execution_result", - "thought_summary", - "thought_signature", - "url_context_call", - "google_search_call", - "mcp_server_tool_call", - "file_search_call", - "google_maps_call", - "function_result", - "url_context_result", - "google_search_result", - "mcp_server_tool_result", - "file_search_result", - "google_maps_result", - "text_annotation", - ].includes(delta.type) - ) { - type = delta.type; - } else if (delta.type === "thought") { - type = "thought_summary"; - } - - if (type) { - const newBlock = { type } as ContentBlock; - block = newBlock; - contentBlocks.set(index, newBlock); - } - } - - if (block) { - if (delta.data) (block as any).data = ((block as any).data || "") + delta.data; - if (delta.mime_type) (block as any).mimeType = delta.mime_type; - if (delta.text) (block as any).text = ((block as any).text || "") + delta.text; - if (delta.signature) (block as any).signature = delta.signature; - - // Tool call arguments - if (delta.name) (block as any).name = delta.name; - if (delta.arguments) { - if (typeof delta.arguments === "string") { - (block as any).arguments = ((block as any).arguments || "") + delta.arguments; - } else { - (block as any).arguments = delta.arguments; - } - } - if (delta.id) (block as any).id = delta.id; - - // Results - if (delta.result) (block as any).result = ((block as any).result || "") + delta.result; - if (delta.is_error !== undefined) (block as any).isError = delta.is_error; - if (delta.call_id) (block as any).callId = delta.call_id; - if (delta.url) (block as any).url = delta.url; - if (delta.query) (block as any).query = delta.query; - if (delta.server) (block as any).server = delta.server; - if (delta.tool) (block as any).tool = delta.tool; - if (delta.annotations) { - (block as any).annotations = (block as any).annotations || []; - (block as any).annotations.push(...delta.annotations); - } - } - } - result.steps[index] = step; - } - } else if (event.type === "step.stop") { - const index = data.index ?? data.step_index; - if (index !== undefined) { - const step = result.steps[index]; - if (step) { - step.status = data.step?.status || "completed"; - } - } - } else if (event.type === "interaction.completed") { - const usage = data.usage || data.interaction?.usage; - if (usage) { - result.usage = { - inputTokens: usage.total_input_tokens ?? usage.input_tokens, - outputTokens: usage.total_output_tokens ?? usage.output_tokens, - thoughtTokens: usage.total_thought_tokens ?? usage.thought_tokens, - cachedTokens: usage.total_cached_tokens ?? usage.cached_tokens, - }; - } - } -} diff --git a/tests/agents/filtering.test.ts b/tests/agents/filtering.test.ts deleted file mode 100644 index 47206cd..0000000 --- a/tests/agents/filtering.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as path from "node:path"; - -describe("agents create inlining integration", () => { - const agentName = `test-filter-agent-${Date.now()}`; - - beforeAll(() => { - // 1. Initialize agent - const initCmd = `bun run src/cli.ts agents init ${agentName} --base-agent antigravity-preview-05-2026`; - execSync(initCmd, { encoding: "utf-8" }); - - // 2. Add some extra files that should be ignored - fs.writeFileSync(path.join(agentName, "ignored_at_root.txt"), "ignore me"); - fs.mkdirSync(path.join(agentName, "ignored_dir"), { recursive: true }); - fs.writeFileSync(path.join(agentName, "ignored_dir", "file.txt"), "ignore me too"); - - // 3. Add some allowed files - fs.writeFileSync(path.join(agentName, "workspace", "allowed.txt"), "keep me"); - fs.mkdirSync(path.join(agentName, "skills", "sub"), { recursive: true }); - fs.writeFileSync(path.join(agentName, "skills", "sub", "allowed_skill.js"), "keep me too"); - }); - - afterAll(() => { - fs.rmSync(agentName, { recursive: true, force: true }); - }); - - test("should only include allowed files in dry-run curl output", () => { - const createCmd = `bun run src/cli.ts agents create --path ./${agentName} --dry-run`; - const stdout = execSync(createCmd, { encoding: "utf-8" }); - - const startIndex = stdout.indexOf("{"); - const endIndex = stdout.lastIndexOf("}"); - expect(startIndex).not.toBe(-1); - expect(endIndex).not.toBe(-1); - - const rawBodyStr = stdout.substring(startIndex, endIndex + 1); - - // Revert bash escaping of single quotes: '\'' -> ' - // In the raw string, this is represented as '\n ... agent'\\''s ...' - // We need to replace matches of: ' (single quote) followed by \' (escaped quote in JS, which is \\' in regex) followed by ' - // Actually, printCurl did: replace(/'/g, "'\\''") - // So we just need to replace "'\\''" with "'" - const bodyStr = rawBodyStr.replace(/'\\''/g, "'"); - - const body = JSON.parse(bodyStr); - - expect(body.id).toBe(agentName); - expect(body.base_environment).toBeDefined(); - expect(body.base_environment.type).toBe("remote"); - expect(body.base_environment.sources).toBeDefined(); - - const sources = body.base_environment.sources; - const targets = sources.map((s: any) => s.target); - console.log("Inlined targets in dry-run:", targets); - - // Allowed - expect(targets).toContain("/.agents/AGENTS.md"); - expect(targets).toContain("/.agents/workspace/allowed.txt"); - expect(targets).toContain("/.agents/skills/sub/allowed_skill.js"); - - // Ignored - expect(targets).not.toContain("/.agents/agent.yaml"); - expect(targets).not.toContain("/.agents/ignored_at_root.txt"); - expect(targets).not.toContain("/.agents/ignored_dir/file.txt"); - - // Total expected sources is 3 - expect(sources.length).toBe(3); - }); -}); diff --git a/tests/agents/lifecycle.test.ts b/tests/agents/lifecycle.test.ts deleted file mode 100644 index e94220c..0000000 --- a/tests/agents/lifecycle.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { afterAll, describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; -import * as fs from "node:fs"; - -describe("agents lifecycle", () => { - const agentName = `test-agent-${Date.now()}`; - - // Helper to run CLI - const runCli = (args: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("init creates directory", () => { - runCli(`agents init ${agentName} --base-agent antigravity-preview-05-2026`); - expect(fs.existsSync(`${agentName}/agent.yaml`)).toBe(true); - expect(fs.existsSync(`${agentName}/AGENTS.md`)).toBe(true); - expect(fs.existsSync(`${agentName}/skills`)).toBe(true); - }); - - test("init is idempotent", () => { - const result = runCli(`agents init ${agentName}`); - expect(result).toContain("already exists"); - }); - - test("create deploys agent", () => { - // Add instructions to agent.yaml to see if it fixes 400 - const yamlPath = `${agentName}/agent.yaml`; - const content = fs.readFileSync(yamlPath, "utf-8"); - fs.writeFileSync(yamlPath, `${content}\ninstructions: You are a helpful assistant.\n`); - const result = runCli(`agents create --path ./${agentName}`); - console.log("Create result:", result); - expect(result).toContain("Created agent"); - }); - - // Blocked: depends on create succeeding - test("list shows deployed agent", () => { - const result = runCli("agents list"); - expect(result).toContain(agentName); - }); - - // Blocked: depends on create succeeding - test("get shows agent details", () => { - const result = runCli(`agents get ${agentName} --json`); - const agent = JSON.parse(result); - expect(agent.id || agent.name).toContain(agentName); - }); - - // Blocked: depends on create succeeding - test("delete removes agent", () => { - const result = runCli(`agents delete ${agentName} --force`); - expect(result).toContain("Deleted agent"); - }); - - // Cleanup - afterAll(() => { - fs.rmSync(agentName, { recursive: true, force: true }); - }); -}); diff --git a/tests/agents/streaming_validation.test.ts b/tests/agents/streaming_validation.test.ts deleted file mode 100644 index 45fd22e..0000000 --- a/tests/agents/streaming_validation.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { spawn } from "node:child_process"; - -describe("gemini-api agents test streaming validation", () => { - test("validates that step.delta is not printed", async () => { - // Start a mock server - const server = Bun.serve({ - port: 0, // random port - fetch(req) { - const url = new URL(req.url); - if (url.pathname === "/interactions") { - // Return SSE stream - const stream = new ReadableStream({ - start(controller) { - const encoder = new TextEncoder(); - const sendEvent = (data: object) => { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); - }; - - sendEvent({ - event_type: "interaction.created", - interaction: { id: "test-id", status: "in_progress" }, - }); - sendEvent({ - event_type: "step.start", - index: 0, - step: { type: "thought", status: "in_progress" }, - }); - sendEvent({ event_type: "step.delta", index: 0, delta: { text: "Thinking delta" } }); - sendEvent({ - event_type: "step.stop", - index: 0, - step: { type: "thought", status: "completed" }, - }); - sendEvent({ event_type: "content.start", index: 1, content: { type: "text" } }); - sendEvent({ - event_type: "content.delta", - index: 1, - delta: { text: "Content delta" }, - }); - sendEvent({ event_type: "content.stop", index: 1 }); - sendEvent({ - event_type: "interaction.completed", - interaction: { id: "test-id", status: "completed" }, - }); - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - controller.close(); - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } - return new Response("Not Found", { status: 404 }); - }, - }); - - const baseUrl = `http://localhost:${server.port}`; - - const child = spawn( - "bun", - [ - "run", - "src/cli.ts", - "agents", - "test", - "--prompt", - "Hello", - "--path", - "./tests/fixtures/agent-configs/valid", - ], - { - env: { ...process.env, GEMINI_API_BASE_URL: baseUrl, GEMINI_API_KEY: "test-key" }, - }, - ); - - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - - child.stderr.on("data", (data) => { - console.error("CLI stderr:", data.toString()); - }); - - const exitCode = await new Promise((resolve) => { - child.on("close", resolve); - }); - - console.log("CLI Output:", output); - console.log("Exit Code:", exitCode); - - server.stop(); - - // Verify that "Content delta" is present - expect(output).toContain("Content delta"); - - // Verify that "Thinking delta" is NOT present (this confirms the bug) - expect(output).not.toContain("Thinking delta"); - }, 30000); -}); diff --git a/tests/agents/test.test.ts b/tests/agents/test.test.ts deleted file mode 100644 index b444503..0000000 --- a/tests/agents/test.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; - -describe("gemini-api agents test", () => { - const runCli = (args: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("test runs interaction with local config fixture", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const result = runCli( - `agents test --prompt "Say exactly: agent-test-pass" --path ./tests/fixtures/agent-configs/valid`, - ); - expect(result).toContain("agent-test-pass"); - }, 60000); -}); diff --git a/tests/api.test.ts b/tests/api.test.ts deleted file mode 100644 index a430ba1..0000000 --- a/tests/api.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { apiRequest, buildInteractionRequest, resolveContext } from "../src/lib/api"; -import { printCurl } from "../src/lib/output"; - -describe("resolveContext", () => { - test("reads from GEMINI_API_KEY", () => { - const oldKey = process.env.GEMINI_API_KEY; - const oldAutopushKey = process.env.GEMINI_AUTOPUSH_API_KEY; - process.env.GEMINI_API_KEY = "test-key"; - delete process.env.GEMINI_AUTOPUSH_API_KEY; - const ctx = resolveContext({}); - expect(ctx.apiKey).toBe("test-key"); - process.env.GEMINI_API_KEY = oldKey; // Restore - process.env.GEMINI_AUTOPUSH_API_KEY = oldAutopushKey; // Restore - }); - - test("flag overrides env var", () => { - const oldKey = process.env.GEMINI_API_KEY; - process.env.GEMINI_API_KEY = "env-key"; - const ctx = resolveContext({ apiKey: "flag-key" }); - expect(ctx.apiKey).toBe("flag-key"); - process.env.GEMINI_API_KEY = oldKey; // Restore - }); - - test("throws when no key", () => { - const oldKey = process.env.GEMINI_API_KEY; - const oldAutopushKey = process.env.GEMINI_AUTOPUSH_API_KEY; - delete process.env.GEMINI_API_KEY; - delete process.env.GEMINI_AUTOPUSH_API_KEY; - expect(() => resolveContext({})).toThrow("No API key found"); - process.env.GEMINI_API_KEY = oldKey; // Restore - process.env.GEMINI_AUTOPUSH_API_KEY = oldAutopushKey; // Restore - }); -}); - -describe("buildInteractionRequest", () => { - test("model interaction", () => { - const body = buildInteractionRequest({ - model: "gemini-3-flash-preview", - input: "Hello", - stream: true, - }); - expect(body).toHaveProperty("model", "gemini-3-flash-preview"); - expect(body).toHaveProperty("input", "Hello"); - expect(body).toHaveProperty("stream", true); - }); - - test("agent interaction", () => { - const body = buildInteractionRequest({ - agent: "my-agent", - input: "Hello", - }); - expect(body).toHaveProperty("agent", "my-agent"); - expect(body).not.toHaveProperty("model"); - }); - test("sources produce environment with type:'remote' instead of config wrapper", () => { - const body = buildInteractionRequest({ - input: "Hello", - sources: [{ type: "gcs", source: "gs://bucket/path", target: "/target" }], - }) as any; - expect(body.environment).toEqual({ - type: "remote", - sources: [{ type: "gcs", source: "gs://bucket/path", target: "/target" }], - }); - expect(body.environment.config).toBeUndefined(); - }); - - test("github type source is normalized to repository", () => { - const body = buildInteractionRequest({ - input: "Hello", - sources: [{ type: "github", source: "https://github.com/foo/bar", target: "/app" }], - }) as any; - expect(body.environment).toEqual({ - type: "remote", - sources: [{ type: "repository", source: "https://github.com/foo/bar", target: "/app" }], - }); - }); - - test("repository type source is parsed correctly", () => { - const body = buildInteractionRequest({ - input: "Hello", - sources: [{ type: "repository", source: "https://github.com/foo/bar", target: "/app" }], - }) as any; - expect(body.environment).toEqual({ - type: "remote", - sources: [{ type: "repository", source: "https://github.com/foo/bar", target: "/app" }], - }); - }); - - test("direct environment mapping for strings", () => { - const bodyRemote = buildInteractionRequest({ - input: "Hello", - environment: "remote", - }) as any; - expect(bodyRemote.environment).toBe("remote"); - - const bodyEnv = buildInteractionRequest({ - input: "Hello", - environment: "env_xyz123", - }) as any; - expect(bodyEnv.environment).toBe("env_xyz123"); - }); - - test("network configuration is preserved and sources normalized in object environments", () => { - const body = buildInteractionRequest({ - input: "Hello", - environment: { - type: "remote", - sources: [{ type: "github", source: "https://github.com/foo/bar", target: "/app" }], - network: "disabled", - }, - }) as any; - expect(body.environment).toEqual({ - type: "remote", - sources: [{ type: "repository", source: "https://github.com/foo/bar", target: "/app" }], - network: "disabled", - }); - }); - - test("outbound network config with allowlist", () => { - const body = buildInteractionRequest({ - input: "Hello", - environment: { - type: "remote", - network: { - allowlist: [{ domain: "api.github.com" }], - }, - }, - }) as any; - expect(body.environment.network).toEqual({ - allowlist: [{ domain: "api.github.com" }], - }); - }); - - test("outbound network config with allowlist and transform rules", () => { - const body = buildInteractionRequest({ - input: "Hello", - environment: { - type: "remote", - network: { - allowlist: [ - { - domain: "api.github.com", - transform: { - "X-Forwarded-For": "1.2.3.4", - }, - }, - ], - }, - }, - }) as any; - expect(body.environment.network).toEqual({ - allowlist: [ - { - domain: "api.github.com", - transform: { - "X-Forwarded-For": "1.2.3.4", - }, - }, - ], - }); - }); - - test("throws error when custom source target is '/'", () => { - expect(() => { - buildInteractionRequest({ - input: "Hello", - sources: [{ type: "gcs", source: "gs://bucket/path", target: "/" }], - }); - }).toThrow('Invalid source target: "/". Custom sources cannot be mounted at root.'); - }); -}); - -describe("Api-Revision header", () => { - test("printCurl includes Api-Revision header for /interactions URL", () => { - const output: string[] = []; - const origLog = console.log; - console.log = (...args: any[]) => output.push(args.join(" ")); - printCurl("POST", "https://example.com/v1beta/interactions", "test-key", { input: "Hello" }); - console.log = origLog; - const curl = output.join("\n"); - expect(curl).toContain("Api-Revision: 2026-05-20"); - }); - - test("printCurl does NOT include Api-Revision for non-interactions URL", () => { - const output: string[] = []; - const origLog = console.log; - console.log = (...args: any[]) => output.push(args.join(" ")); - printCurl("GET", "https://example.com/v1beta/agents", "test-key"); - console.log = origLog; - const curl = output.join("\n"); - expect(curl).not.toContain("Api-Revision"); - }); -}); - -// Integration test (requires GEMINI_API_KEY) -describe("apiRequest (live API)", () => { - test("GET /agents returns list", async () => { - // Only run if GEMINI_API_KEY is set - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const ctx = resolveContext({}); - try { - const result = await apiRequest<{ agents?: any[] }>(ctx, "GET", "/agents"); - expect(result).toHaveProperty("agents"); - } catch (e) { - // If it fails with 404 or something else, it might be because the endpoint is not available in sandbox yet - // or we need to create an agent first. - // But the task implies it should work. - console.error("apiRequest failed:", e); - throw e; - } - }); -}); diff --git a/tests/collect_files.test.ts b/tests/collect_files.test.ts deleted file mode 100644 index a63ac23..0000000 --- a/tests/collect_files.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { collectInlineFiles } from "../src/lib/files"; - -describe("collectInlineFiles filtering", () => { - const testDir = path.join(__dirname, "tmp-test-agent"); - - beforeAll(() => { - // Setup test directory structure - fs.mkdirSync(testDir, { recursive: true }); - fs.mkdirSync(path.join(testDir, "workspace"), { recursive: true }); - fs.mkdirSync(path.join(testDir, "skills"), { recursive: true }); - fs.mkdirSync(path.join(testDir, "ignored_dir"), { recursive: true }); - - // Allowed files - fs.writeFileSync(path.join(testDir, "AGENTS.md"), "agents content"); - - fs.writeFileSync(path.join(testDir, "workspace", "file1.txt"), "file1 content"); - fs.writeFileSync(path.join(testDir, "skills", "skill1.js"), "skill1 content"); - - // Ignored files - fs.writeFileSync(path.join(testDir, "agent.yaml"), "agent config"); - fs.writeFileSync(path.join(testDir, "ignored_file.txt"), "ignored file"); - fs.writeFileSync(path.join(testDir, "ignored_dir", "file2.txt"), "ignored dir file"); - fs.writeFileSync(path.join(testDir, "package.json"), "{}"); - }); - - afterAll(() => { - // Cleanup - fs.rmSync(testDir, { recursive: true, force: true }); - }); - - test("should only collect allowed files", async () => { - const files = await collectInlineFiles(testDir); - - const targets = files.map((f) => f.target); - console.log("Collected targets:", targets); - - expect(targets).toContain("/.agents/AGENTS.md"); - expect(targets).toContain("/.agents/workspace/file1.txt"); - expect(targets).toContain("/.agents/skills/skill1.js"); - - // Should NOT contain ignored files - expect(targets).not.toContain("/.agents/agent.yaml"); - expect(targets).not.toContain("/.agents/ignored_file.txt"); - expect(targets).not.toContain("/.agents/ignored_dir/file2.txt"); - expect(targets).not.toContain("/.agents/package.json"); - - // Total expected allowed files is 3 - expect(files.length).toBe(3); - }); -}); diff --git a/tests/config.test.ts b/tests/config.test.ts deleted file mode 100644 index 6636dee..0000000 --- a/tests/config.test.ts +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { loadAgent } from "../src/lib/config"; -import { AgentConfigSchema } from "../src/lib/schemas"; - -function restoreEnv(name: string, value: string | undefined) { - if (value === undefined) { - delete process.env[name]; - } else { - process.env[name] = value; - } -} - -describe("AgentConfigSchema", () => { - test("valid minimal config", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - base_agent: "antigravity-preview-05-2026", - }); - expect(result.success).toBe(true); - }); - - test("valid full config", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - base_agent: "antigravity-preview-05-2026", - description: "Test agent", - instructions: "You are helpful", - tools: [{ type: "code_execution" }, { type: "google_search" }], - }); - expect(result.success).toBe(true); - }); - - test("valid base_environment as string", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - base_environment: "env-123", - }); - expect(result.success).toBe(true); - }); - - test("valid base_environment with remote sources and network", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - base_environment: { - type: "remote", - sources: [{ type: "gcs", source: "gs://bucket/path", target: "/target" }], - network: { allowlist: [{ domain: "example.com" }] }, - }, - }); - expect(result.success).toBe(true); - }); - - test("missing id fails", () => { - const result = AgentConfigSchema.safeParse({ - base_agent: "antigravity-preview-05-2026", - }); - expect(result.success).toBe(false); - }); - - test("invalid tool type fails", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - tools: [{ type: "invalid_tool" }], - }); - expect(result.success).toBe(false); - }); - - test("empty object fails", () => { - const result = AgentConfigSchema.safeParse({}); - expect(result.success).toBe(false); - }); - - test("unknown fields are rejected", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - unknown_field: "value", - }); - expect(result.success).toBe(false); - }); - - test("valid config with examples", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - base_agent: "antigravity-preview-05-2026", - examples: [ - { title: "Write a poem", prompt: "Write a short poem about coding" }, - { title: "Explain AI", prompt: "Explain artificial intelligence" }, - ], - }); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.examples).toHaveLength(2); - expect(result.data.examples?.[0].title).toBe("Write a poem"); - expect(result.data.examples?.[0].prompt).toBe("Write a short poem about coding"); - } - }); - - test("examples with missing prompt fails", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - examples: [ - { title: "Write a poem" }, // missing prompt - ], - }); - expect(result.success).toBe(false); - }); - - test("examples with missing title fails", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - examples: [ - { prompt: "Write something" }, // missing title - ], - }); - expect(result.success).toBe(false); - }); - - test("valid base_environment with type:remote format", () => { - const result = AgentConfigSchema.safeParse({ - id: "my-agent", - base_environment: { - type: "remote", - sources: [{ type: "gcs", source: "gs://bucket/path", target: "/target" }], - }, - }); - expect(result.success).toBe(true); - }); -}); - -describe("loadAgent", () => { - test("loads valid agent.yaml from fixture", async () => { - const agent = await loadAgent("./tests/fixtures/agent-configs/valid"); - expect(agent.config.id).toBe("test-agent"); - }); - - test("throws ConfigError for missing file", async () => { - expect(loadAgent("./nonexistent")).rejects.toThrow(); - }); - - test("throws ConfigError for invalid yaml", async () => { - expect(loadAgent("./tests/fixtures/agent-configs/invalid")).rejects.toThrow(); - }); - - test("throws ConfigError for malformed yaml", async () => { - expect(loadAgent("./tests/fixtures/agent-configs/malformed")).rejects.toThrow(); - }); - - test("throws ConfigError for empty yaml", async () => { - expect(loadAgent("./tests/fixtures/agent-configs/empty")).rejects.toThrow(); - }); - - test("loads agent.yaml with examples from fixture", async () => { - const agent = await loadAgent("./tests/fixtures/agent-configs/with-examples"); - expect(agent.config.id).toBe("test-agent-examples"); - expect(agent.config.examples).toHaveLength(2); - expect(agent.config.examples?.[0].title).toBe("Write a poem"); - }); - - test("resolves agent.yaml environment variable placeholders from process env", async () => { - const oldGithubToken = process.env.GITHUB_TOKEN; - const oldGithubPat = process.env.GITHUB_PAT; - const oldGeminiApiKey = process.env.GEMINI_API_KEY; - try { - process.env.GITHUB_TOKEN = 'process-"github"-token'; - process.env.GITHUB_PAT = "process-$&-github-pat"; - process.env.GEMINI_API_KEY = "process-gemini-api-key"; - - const agent = await loadAgent("./tests/fixtures/agent-configs/with-env-vars"); - expect(agent.config.id).toBe("test-agent-with-env-vars"); - expect(agent.config.sources).toEqual([ - { - type: "github", - source: "https://process-$&-github-pat@github.com/my-org/private-repo", - target: "/workspace/private-repo", - }, - ]); - expect(agent.config.environment).toEqual({ - type: "remote", - network: { - allowlist: [ - { - domain: "api.github.com", - transform: { - Authorization: 'Bearer process-"github"-token', - "X-GitHub-Token": 'process-"github"-token', - }, - }, - { - domain: "generativelanguage.googleapis.com", - transform: { - "x-goog-api-key": "process-gemini-api-key", - Authorization: "Bearer process-gemini-api-key", - }, - }, - ], - }, - }); - } finally { - restoreEnv("GITHUB_TOKEN", oldGithubToken); - restoreEnv("GITHUB_PAT", oldGithubPat); - restoreEnv("GEMINI_API_KEY", oldGeminiApiKey); - } - }); - - test("resolves agent.yaml environment variable placeholders from env file first", async () => { - const oldGithubToken = process.env.GITHUB_TOKEN; - const oldGithubPat = process.env.GITHUB_PAT; - const oldGeminiApiKey = process.env.GEMINI_API_KEY; - try { - process.env.GITHUB_TOKEN = "process-github-token"; - process.env.GITHUB_PAT = "process-github-pat"; - process.env.GEMINI_API_KEY = "process-gemini-api-key"; - - const agent = await loadAgent("./tests/fixtures/agent-configs/with-env-vars", { - envFile: "./tests/fixtures/agent-configs/with-env-vars/.env", - }); - expect(agent.config.sources).toEqual([ - { - type: "github", - source: "https://env-file-github-pat@github.com/my-org/private-repo", - target: "/workspace/private-repo", - }, - ]); - expect(agent.config.environment).toEqual({ - type: "remote", - network: { - allowlist: [ - { - domain: "api.github.com", - transform: { - Authorization: "Bearer env-file-github-token", - "X-GitHub-Token": "env-file-github-token", - }, - }, - { - domain: "generativelanguage.googleapis.com", - transform: { - "x-goog-api-key": "env-file-gemini-api-key", - Authorization: "Bearer env-file-gemini-api-key", - }, - }, - ], - }, - }); - } finally { - restoreEnv("GITHUB_TOKEN", oldGithubToken); - restoreEnv("GITHUB_PAT", oldGithubPat); - restoreEnv("GEMINI_API_KEY", oldGeminiApiKey); - } - }); - - test("throws when agent.yaml references missing environment variables", async () => { - const oldGithubToken = process.env.GITHUB_TOKEN; - const oldGithubPat = process.env.GITHUB_PAT; - const oldGeminiApiKey = process.env.GEMINI_API_KEY; - try { - delete process.env.GITHUB_TOKEN; - delete process.env.GITHUB_PAT; - delete process.env.GEMINI_API_KEY; - - await expect(loadAgent("./tests/fixtures/agent-configs/with-env-vars")).rejects.toThrow( - "Missing environment variable GITHUB_PAT", - ); - } finally { - restoreEnv("GITHUB_TOKEN", oldGithubToken); - restoreEnv("GITHUB_PAT", oldGithubPat); - restoreEnv("GEMINI_API_KEY", oldGeminiApiKey); - } - }); - - test("throws a clear error when env file is missing", async () => { - await expect( - loadAgent("./tests/fixtures/agent-configs/valid", { - envFile: "./tests/fixtures/agent-configs/valid/missing.env", - }), - ).rejects.toThrow( - "Environment file not found: ./tests/fixtures/agent-configs/valid/missing.env", - ); - }); -}); diff --git a/tests/e2e/E2E.md b/tests/e2e/E2E.md deleted file mode 100644 index 44951d4..0000000 --- a/tests/e2e/E2E.md +++ /dev/null @@ -1,798 +0,0 @@ -# End-to-End Test Plan — `gemini-api` CLI - -> Pre-release validation suite. Every test has a `--dry-run` variant (fast, no API call) and a live variant (real API call). -> -> **Run all dry-run tests:** `bash E2E.sh --dry-run` -> **Run all live tests:** `bash E2E.sh` - ---- - -## Convention - -Each test case below uses this format: - -``` -CUJ-XX: -API: <Endpoint or feature being tested> -dry-run: <command with --dry-run> -live: <command that hits the real API> -assert: <what to check in stdout/stderr> -``` - -The CLI entry point is `bun run src/cli.ts` during development or `gemini-api` when installed. -All commands below use the `CLI` placeholder — set it in your test script: - -```bash -export GEMINI_API_KEY="your-api-key" - -CLI="bun run src/cli.ts" -# or -CLI="gemini-api" -``` - ---- - -## 1. Basic Interactions (Model) - -### CUJ-01: Simple text prompt (non-streaming) - -The most basic interaction — send a text prompt, get a text response. - -```bash -# dry-run -$CLI run "What is 2+2?" --dry-run - -# live -$CLI run "What is 2+2?" -``` - -**Assert (dry-run):** Output contains `curl -X POST`, `/interactions`, `"input"`, `"model": "gemini-3-flash-preview"`. -**Assert (live):** Output contains `✓ completed`, `interaction_id:`. - ---- - -### CUJ-02: Simple text prompt (streaming) - -Default streaming mode — text arrives incrementally. - -```bash -# dry-run -$CLI run "Count to 5" --dry-run - -# live -$CLI run "Count to 5" -``` - -**Assert (dry-run):** Output contains `curl -X POST`, `"stream": true`. -**Assert (live):** Output contains `1`, `5`, `✓ completed`. - ---- - -### CUJ-03: Specify a model explicitly - -Override the default model. - -```bash -# dry-run -$CLI run "Hello" --model gemini-3.1-pro-preview --dry-run - -# live -$CLI run "Hello" --model gemini-3.1-pro-preview -``` - -**Assert (dry-run):** Output contains `"model": "gemini-3.1-pro-preview"`. -**Assert (live):** Output contains `✓ completed`. - ---- - -### CUJ-04: JSON output mode - -Raw SSE events as JSONL for machine consumption. - -```bash -# dry-run -$CLI run "Hello" --json --dry-run - -# live -$CLI run "Say hi" --json -``` - -**Assert (dry-run):** Output contains `curl`. -**Assert (live):** Each line is valid JSON. First event has `event_type`. At least one `content.delta` event exists. - ---- - - - -### CUJ-06: System instruction - -Provide a system prompt alongside the user prompt. - -```bash -# dry-run -$CLI run "What are you?" --system-instruction "You are a pirate. Always respond in pirate speak." --dry-run - -# live -$CLI run "What are you?" --system-instruction "You are a pirate. Always respond in pirate speak." -``` - -**Assert (dry-run):** Output contains `"system_instruction"`, `pirate`. -**Assert (live):** Output contains pirate-like language (e.g., `arr`, `matey`, `pirate`). - ---- - -### CUJ-07: Service tier (flex) - -Use the flex service tier for cost optimization. - -```bash -# dry-run -$CLI run "Hello" --service-tier flex --dry-run - -# live -$CLI run "Hello" --service-tier flex -``` - -**Assert (dry-run):** Output contains `"service_tier": "flex"`. -**Assert (live):** Output contains `✓ completed`. - ---- - -## 2. Multi-Turn Conversations - -### CUJ-08: Stateful multi-turn with previous-interaction-id - -Continue a conversation across two turns using server-side state. - -```bash -# dry-run -$CLI run "What was the word?" --previous-interaction-id fake_id_123 --dry-run - -# live (two-step) -# Turn 1: -RESULT=$($CLI run "Remember the word: banana" --json 2>&1) -INT_ID=$(echo "$RESULT" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) - -# Turn 2: -$CLI run "What word did I ask you to remember?" --previous-interaction-id "$INT_ID" -``` - -**Assert (dry-run):** Output contains `"previous_interaction_id": "fake_id_123"`. -**Assert (live):** Turn 2 output contains `banana`. - ---- - -## 3. Multimodal Input - -### CUJ-09: Image understanding - -Send an image file alongside a text prompt. - -```bash -# Create a test image (1x1 red PNG) -echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" | base64 -d > /tmp/test.png - -# dry-run -$CLI run "What color is this?" --input image:/tmp/test.png --dry-run - -# live -$CLI run "What color is this?" --input image:/tmp/test.png -``` - -**Assert (dry-run):** Output contains `"type": "image"`, `"mime_type": "image/png"`, base64 data. -**Assert (live):** Output mentions a color (e.g., `red`, `salmon`). - ---- - -### CUJ-10: Missing input file error - -Graceful error when the input file doesn't exist. - -```bash -# No API call needed — this is a client-side validation -$CLI run "Hello" --input image:nonexistent.png -``` - -**Assert:** Output contains `File not found`. - ---- - -## 4. Multimodal Output (Generation) - -### CUJ-11: Image generation - -Generate an image and save to disk. - -```bash -# dry-run -$CLI run "A blue square" --model gemini-3-pro-image-preview --output /tmp/test_gen.png --dry-run - -# live -$CLI run "Generate a simple blue square" --model gemini-3-pro-image-preview --output /tmp/test_gen.png -``` - -**Assert (dry-run):** Output contains `"model": "gemini-3-pro-image-preview"`. -**Assert (live):** File `/tmp/test_gen.png` exists and is > 100 bytes. - ---- - -### CUJ-12: Image generation with config (aspect ratio, size) - -Use image_config to control output dimensions. - -```bash -# dry-run -$CLI run "A sunset" --model gemini-3-pro-image-preview --aspect-ratio 16:9 --image-size 2k --dry-run - -# live -$CLI run "A sunset over mountains" --model gemini-3-pro-image-preview --aspect-ratio 16:9 --image-size 2k --output /tmp/test_sunset.png -``` - -**Assert (dry-run):** Output contains `"image_config"`, `"aspect_ratio": "16:9"`, `"image_size": "2k"`. -**Assert (live):** File `/tmp/test_sunset.png` exists and is > 100 bytes. - ---- - -### CUJ-13: Text-to-speech (TTS) - -Generate speech audio from text. - -```bash -# dry-run -$CLI run "Hello world" --model gemini-3.1-flash-tts-preview --voice Kore --language en-US --output /tmp/test_tts.wav --dry-run - -# live -$CLI run "Hello world" --model gemini-3.1-flash-tts-preview --voice Kore --output /tmp/test_tts.wav -``` - -**Assert (dry-run):** Output contains `"speech_config"`, `"voice": "Kore"`. -**Assert (live):** File `/tmp/test_tts.wav` exists and is > 100 bytes. - ---- - -### CUJ-14: Image editing (input image + output image) - -Edit an existing image. - -```bash -# Create test image -echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" | base64 -d > /tmp/test_edit.png - -# dry-run -$CLI run "Make it green" --input image:/tmp/test_edit.png --response-modality image --model gemini-3-pro-image-preview --output /tmp/test_edited.png --dry-run - -# live -$CLI run "Make this image green" --input image:/tmp/test_edit.png --response-modality image --model gemini-3-pro-image-preview --output /tmp/test_edited.png -``` - -**Assert (dry-run):** Output contains `"response_modalities"`, `"image"`. -**Assert (live):** File `/tmp/test_edited.png` exists and is > 100 bytes. - ---- - -### CUJ-15: Image editing with edit_strength and mask - -Advanced image editing with strength control and mask. - -```bash -# dry-run -echo "dummy" > /tmp/tmp_input.png -echo "dummy" > /tmp/tmp_mask.png -$CLI run "Edit this" --input image:/tmp/tmp_input.png --response-modality image --edit-strength 0.5 --mask /tmp/tmp_mask.png --dry-run -``` - -**Assert (dry-run):** Output contains `"edit_strength": 0.5`, `"mask":`. - ---- - -## 5. Tools - -### CUJ-16: Code execution tool - -Use code_execution to calculate something. - -```bash -# dry-run -$CLI run "Calculate 2+2" --tool code_execution --dry-run - -# live -$CLI run "Use code execution to calculate 2+2 and return only the number" --tool code_execution -``` - -**Assert (dry-run):** Output contains `"type": "code_execution"`. -**Assert (live):** Output contains `4`, no `API error`. - ---- - -### CUJ-17: Google Search tool - -Use Google Search for grounding. - -```bash -# dry-run -$CLI run "What happened today?" --tool google_search --dry-run - -# live -$CLI run "What is the current population of Tokyo? Use search." --tool google_search -``` - -**Assert (dry-run):** Output contains `"type": "google_search"`. -**Assert (live):** Output contains `✓ completed`, no `API error`. - ---- - -### CUJ-18: URL context tool - -Fetch and summarize a URL. - -```bash -# dry-run -$CLI run "Summarize https://www.wikipedia.org/" --tool url_context --dry-run - -# live -$CLI run "Summarize the content of https://www.wikipedia.org/" --tool url_context -``` - -**Assert (dry-run):** Output contains `"type": "url_context"`. -**Assert (live):** Output contains `✓ completed`, mentions Wikipedia. - ---- - -### CUJ-19: Multiple tools together - -Combine Google Search and code execution. - -```bash -# dry-run -$CLI run "Search and calculate" --tool google_search --tool code_execution --dry-run - -# live -$CLI run "Search for the GDP of France then calculate GDP per capita" --tool google_search --tool code_execution -``` - -**Assert (dry-run):** Output contains both `google_search` and `code_execution`. -**Assert (live):** Output contains `✓ completed`. - ---- - -### CUJ-20: Invalid tool error - -Graceful error for unknown tool names. - -```bash -$CLI run "Hello" --tool invalid_tool -``` - -**Assert:** Output contains `Unknown tool`, lists available tools (e.g., `code_execution`). - ---- - -## 6. Agents Lifecycle - -### CUJ-21: Agent init (scaffold) - -Create a new agent project directory. - -```bash -AGENT_NAME="e2e-test-agent-$(date +%s)" - -$CLI agents init "$AGENT_NAME" -``` - -**Assert:** Directory `$AGENT_NAME/` exists with `agent.yaml`, `AGENTS.md`, `skills/`, `.env`. - -**Cleanup:** `rm -rf "$AGENT_NAME"` - ---- - -### CUJ-22: Agent init is idempotent - -Running init twice on the same name reports "already exists". - -```bash -$CLI agents init "$AGENT_NAME" -$CLI agents init "$AGENT_NAME" -``` - -**Assert:** Second run output contains `already exists`. - ---- - -### CUJ-23: Agent create (deploy) — dry-run - -Deploy an agent from a directory. - -```bash -$CLI agents init "$AGENT_NAME" -$CLI agents create --path "./$AGENT_NAME" --dry-run -``` - -**Assert:** Output contains `curl -X POST`, `/agents`, `"id":`, `"base_agent": "antigravity-preview-05-2026"`. - ---- - -### CUJ-24: Agent create (deploy) — live - -```bash -$CLI agents init "$AGENT_NAME" -$CLI agents create --path "./$AGENT_NAME" -``` - -**Assert:** Output contains `✓ Created agent`. - ---- - -### CUJ-25: Agent list - -List all deployed agents. - -```bash -# dry-run -$CLI agents list --dry-run - -# live -$CLI agents list -``` - -**Assert (dry-run):** Output contains `curl`, `/agents`. -**Assert (live):** Output is a list (JSON or human-readable) containing agent names. - ---- - -### CUJ-26: Agent list (JSON mode) - -```bash -# dry-run -$CLI agents list --json --dry-run - -# live -$CLI agents list --json -``` - -**Assert (live):** Output is valid JSON. - ---- - -### CUJ-27: Agent get - -Get details of a specific agent. - -```bash -# dry-run -$CLI agents get my-agent --dry-run - -# live -$CLI agents get "$AGENT_NAME" -``` - -**Assert (dry-run):** Output contains `curl`, `/agents/my-agent`. -**Assert (live):** Output contains agent name/id and `base_agent`. - ---- - -### CUJ-28: Agent delete - -Delete a deployed agent. - -```bash -# dry-run -$CLI agents delete my-agent --force --dry-run - -# live -$CLI agents delete "$AGENT_NAME" --force -``` - -**Assert (dry-run):** Output contains `curl -X DELETE`, `/agents/my-agent`. -**Assert (live):** Output contains `Deleted agent`. - ---- - -### CUJ-29: Agent full lifecycle (create → list → get → delete) - -End-to-end lifecycle test. - -```bash -AGENT_NAME="e2e-lifecycle-$(date +%s)" - -# 1. Init -$CLI agents init "$AGENT_NAME" - -# 2. Create -$CLI agents create --path "./$AGENT_NAME" - -# 3. List — should include the agent -$CLI agents list - -# 4. Get — should return details -$CLI agents get "$AGENT_NAME" - -# 5. Delete -$CLI agents delete "$AGENT_NAME" --force - -# 6. Cleanup -rm -rf "$AGENT_NAME" -``` - -**Assert:** Each step succeeds. Agent appears in list after create, disappears after delete. - ---- - -### CUJ-30: Agent test (interaction via local config) - -Run an interaction using local agent.yaml. - -```bash -AGENT_NAME="e2e-test-$(date +%s)" -$CLI agents init "$AGENT_NAME" - -# dry-run -$CLI agents test --prompt "Hello" --path "./$AGENT_NAME" --dry-run - -# live -$CLI agents test --prompt "Hello" --path "./$AGENT_NAME" - -rm -rf "$AGENT_NAME" -``` - -**Assert (dry-run):** Output contains `curl`, `/interactions`, `"agent": "antigravity-preview-05-2026"`. -**Assert (live):** Output contains `✓ completed`. - ---- - -## 7. Agent Interactions - -### CUJ-31: Run with deployed agent (antigravity-preview-05-2026) - -Interact with the base antigravity-preview-05-2026 agent. - -```bash -# dry-run -$CLI run "What is 2+2?" --agent antigravity-preview-05-2026 --dry-run - -# live -$CLI run "What is 2+2?" --agent antigravity-preview-05-2026 -``` - -**Assert (dry-run):** Output contains `"agent": "antigravity-preview-05-2026"`, `"environment": {"enabled": true}`. -**Assert (live):** Output contains `✓ completed`, environment_id. - ---- - -### CUJ-32: Run with custom agent - -Interact with a user-created agent. - -```bash -# dry-run -$CLI run "Hello" --agent my-custom-agent --dry-run - -# live (requires agent to exist) -$CLI run "Hello" --agent "$AGENT_NAME" -``` - -**Assert (dry-run):** Output contains `"agent": "my-custom-agent"`. -**Assert (live):** Output contains `✓ completed`. - ---- - -### CUJ-33: Agent with environment persistence (multi-turn) - -Multi-turn agent interaction that reuses an environment. - -```bash -# dry-run -$CLI run "Continue" --agent antigravity-preview-05-2026 --previous-interaction-id fake_int --dry-run - -# live (two-step) -RESULT=$($CLI run "Write 'hello' to /tmp/test.txt" --agent antigravity-preview-05-2026 --json 2>&1) -INT_ID=$(echo "$RESULT" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) - -$CLI run "Read /tmp/test.txt and tell me what it says" --agent antigravity-preview-05-2026 --previous-interaction-id "$INT_ID" -``` - -**Assert (dry-run):** Output contains `"previous_interaction_id"`. -**Assert (live):** Second turn output mentions `hello`. - ---- - -## 8. Deep Research Agent - -### CUJ-34: Deep Research (background mode) - -Start a Deep Research agent task. - -```bash -# dry-run -$CLI run "Research the history of TPUs" --agent deep-research-preview-04-2026 --dry-run - -# live (will take minutes) -$CLI run "Research the history of Google TPUs in 2 paragraphs" --agent deep-research-preview-04-2026 -``` - -**Assert (dry-run):** Output contains `"agent": "deep-research-preview-04-2026"`, `"background": true`, `"agent_config"`. -**Assert (live):** Output contains final research text, `✓ completed`. - ---- - -## 9. Files (Environment) - -### CUJ-36: Download environment files (snapshot) - -```bash -# dry-run -$CLI files download env_fake123 --dry-run - -# live (requires a valid env_id) -# $CLI files download "$ENV_ID" --output ./tmp -``` - -**Assert (dry-run):** Output contains `curl`, `/files/environment-env_fake123:download?alt=media`. -**Assert (live):** Directory `./tmp/snapshot_$ENV_ID` exists and contains files. - ---- - -## 10. Error Handling - -### CUJ-37: Missing prompt - -```bash -$CLI run -``` - -**Assert:** Output contains `Missing prompt`, exit code ≠ 0. - ---- - -### CUJ-38: Missing API key - -```bash -GEMINI_API_KEY="" GEMINI_AUTOPUSH_API_KEY="" $CLI run "Hello" -``` - -**Assert:** Output contains `No API key found`, exit code ≠ 0. - ---- - -### CUJ-39: Invalid model (400 error) - -```bash -$CLI run "Hello" --model nonexistent-model -``` - -**Assert:** Output contains `API error` or `400`. - ---- - -### CUJ-40: Missing agent.yaml for agents create - -```bash -$CLI agents create --path /tmp/empty-dir-that-does-not-exist -``` - -**Assert:** Output contains error about missing `agent.yaml`. - ---- - -### CUJ-44: Invalid agent error - -Graceful error for unknown agent names. - -```bash -$CLI run "Hello" --agent invalid_agent -``` - -**Assert:** Output contains `Unknown agent`, lists available agent types (e.g., `antigravity-preview-05-2026`). - ---- - -## 11. Output & UX - -### CUJ-41: Dry-run on all commands - -Every command that hits the API must support `--dry-run`. - -```bash -$CLI run "Hello" --dry-run -$CLI agents create --dry-run -$CLI agents list --dry-run -$CLI agents get my-agent --dry-run -$CLI agents delete my-agent --force --dry-run -$CLI files list env_123 --dry-run -$CLI files download env_123 --dry-run -``` - -**Assert:** Each outputs a `curl` command and exits with code 0. - ---- - -### CUJ-42: Completion summary metadata - -The human-mode completion summary includes machine-useful metadata. - -```bash -$CLI run "Say hello" -``` - -**Assert:** Output contains `✓ completed`, `interaction_id:`, `latency:`. - ---- - -### CUJ-43: Interaction logging - -Every interaction creates a JSONL log file. - -```bash -rm -rf .gemini/logs -$CLI run "Hello" -ls .gemini/logs/ -``` - -**Assert:** A `.jsonl` file exists in `.gemini/logs/`. File has 2 lines (request + response). - ---- - -### CUJ-45: Agent test with network transform (dry-run) - -Verify that outbound network configurations and secure header transform secrets are preserved and serialized correctly in local agent test dry-runs. - -```bash -# Setup -$CLI agents init test-agent -# (Configure custom network transforms in test-agent/agent.yaml) - -# dry-run test -$CLI agents test --prompt "Hello" --path "./test-agent" --dry-run -``` - -**Assert:** Output contains the parsed `network` block with its associated domains and injected header structures (e.g. `Authorization`). - ---- - -## Summary Table - -| # | CUJ | Category | -|---|-----|----------| -| [01](./cuj_01.sh) | Simple text (non-streaming) | Interactions | -| [02](./cuj_02.sh) | Simple text (streaming) | Interactions | -| [03](./cuj_03.sh) | Specify model | Interactions | -| [04](./cuj_04.sh) | JSON output | Interactions | -| [06](./cuj_06.sh) | System instruction | Interactions | -| [07](./cuj_07.sh) | Service tier | Interactions | -| [08](./cuj_08.sh) | Multi-turn | Conversations | -| [09](./cuj_09.sh) | Image understanding | Multimodal | -| [10](./cuj_10.sh) | Missing file error | Error | -| [11](./cuj_11.sh) | Image generation | Generation | -| [12](./cuj_12.sh) | Image config | Generation | -| [13](./cuj_13.sh) | TTS | Generation | -| [14](./cuj_14.sh) | Image editing | Generation | -| [15](./cuj_15.sh) | Edit strength + mask | Generation | -| [16](./cuj_16.sh) | Code execution | Tools | -| [17](./cuj_17.sh) | Google Search | Tools | -| [18](./cuj_18.sh) | URL context | Tools | -| [19](./cuj_19.sh) | Multiple tools | Tools | -| [20](./cuj_20.sh) | Invalid tool | Error | -| [21](./cuj_21.sh) | Agent init | Agents | -| [22](./cuj_22.sh) | Agent init idempotent | Agents | -| [23](./cuj_23.sh) | Agent create (dry) | Agents | -| [24](./cuj_24.sh) | Agent create (live) | Agents | -| [25](./cuj_25.sh) | Agent list | Agents | -| [26](./cuj_26.sh) | Agent list JSON | Agents | -| [27](./cuj_27.sh) | Agent get | Agents | -| [28](./cuj_28.sh) | Agent delete | Agents | -| [29](./cuj_29.sh) | Agent full lifecycle | Agents | -| [30](./cuj_30.sh) | Agent test | Agents | -| [31](./cuj_31.sh) | antigravity-preview-05-2026 agent | Agent Run | -| [32](./cuj_32.sh) | Custom agent | Agent Run | -| [33](./cuj_33.sh) | Agent env persistence | Agent Run | -| [34](./cuj_34.sh) | Deep Research | Agent Run | -| [35](./cuj_35.sh) | Files list | Files | -| [36](./cuj_36.sh) | Files download | Files | -| [37](./cuj_37.sh) | Missing prompt | Error | -| [38](./cuj_38.sh) | Missing API key | Error | -| [39](./cuj_39.sh) | Invalid model | Error | -| [40](./cuj_40.sh) | Missing agent.yaml | Error | -| [41](./cuj_41.sh) | Dry-run on all commands | UX | -| [42](./cuj_42.sh) | Completion summary | UX | -| [43](./cuj_43.sh) | Interaction logging | UX | -| [44](./cuj_44.sh) | Invalid agent | Error | -| [45](./cuj_45.sh) | Network transform dry-run | Agent Run | diff --git a/tests/e2e/cuj_01.sh b/tests/e2e/cuj_01.sh deleted file mode 100755 index 930bd3e..0000000 --- a/tests/e2e/cuj_01.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-01: Simple text prompt (non-streaming) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-01: Simple text prompt (non-streaming) ===" - -# dry-run -$CLI run "What is 2+2?" --dry-run - -# live -$CLI run "What is 2+2?" diff --git a/tests/e2e/cuj_02.sh b/tests/e2e/cuj_02.sh deleted file mode 100755 index c307d04..0000000 --- a/tests/e2e/cuj_02.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-02: Simple text prompt (streaming) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-02: Simple text prompt (streaming) ===" - -# dry-run -$CLI run "Count to 5" --dry-run - -# live -$CLI run "Count to 5" diff --git a/tests/e2e/cuj_03.sh b/tests/e2e/cuj_03.sh deleted file mode 100755 index ca85a32..0000000 --- a/tests/e2e/cuj_03.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-03: Specify a model explicitly -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-03: Specify a model explicitly ===" - -# dry-run -$CLI run "Hello" --model gemini-3.1-flash-lite-preview --dry-run - -# live -$CLI run "Hello" --model gemini-3.1-flash-lite-preview diff --git a/tests/e2e/cuj_04.sh b/tests/e2e/cuj_04.sh deleted file mode 100755 index b3d4646..0000000 --- a/tests/e2e/cuj_04.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-04: JSON output mode -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-04: JSON output mode ===" - -# dry-run -$CLI run "Hello" --json --dry-run - -# live -$CLI run "Say hi" --json diff --git a/tests/e2e/cuj_06.sh b/tests/e2e/cuj_06.sh deleted file mode 100755 index 34afb63..0000000 --- a/tests/e2e/cuj_06.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-06: System instruction -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-06: System instruction ===" - -# dry-run -$CLI run "What are you?" --system-instruction "You are a pirate. Always respond in pirate speak." --dry-run - -# live -$CLI run "What are you?" --system-instruction "You are a pirate. Always respond in pirate speak." diff --git a/tests/e2e/cuj_07.sh b/tests/e2e/cuj_07.sh deleted file mode 100755 index 60ca35a..0000000 --- a/tests/e2e/cuj_07.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-07: Service tier (flex) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-07: Service tier (flex) ===" - -# dry-run -$CLI run "Hello" --service-tier flex --dry-run - -# live -$CLI run "Hello" --service-tier flex diff --git a/tests/e2e/cuj_08.sh b/tests/e2e/cuj_08.sh deleted file mode 100755 index 2d2c542..0000000 --- a/tests/e2e/cuj_08.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-08: Stateful multi-turn with previous-interaction-id -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-08: Stateful multi-turn with previous-interaction-id ===" - -# dry-run -$CLI run "What was the word?" --previous-interaction-id fake_id_123 --dry-run - -# live (two-step) -# Turn 1: -RESULT=$($CLI run "Remember the word: banana" --json 2>&1) -INT_ID=$(echo "$RESULT" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) - -# Turn 2: -$CLI run "What word did I ask you to remember?" --previous-interaction-id "$INT_ID" diff --git a/tests/e2e/cuj_09.sh b/tests/e2e/cuj_09.sh deleted file mode 100755 index 12f7736..0000000 --- a/tests/e2e/cuj_09.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-09: Image understanding -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-09: Image understanding ===" - -# Create a test image (1x1 red PNG) -echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" | base64 -d > tmp/test.png - -# dry-run -$CLI run "What color is this?" --input image:tmp/test.png --dry-run - -# live -$CLI run "What color is this?" --input image:tmp/test.png diff --git a/tests/e2e/cuj_10.sh b/tests/e2e/cuj_10.sh deleted file mode 100755 index 147db22..0000000 --- a/tests/e2e/cuj_10.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-10: Missing input file error -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-10: Missing input file error ===" - -# No API call needed — this is a client-side validation -! $CLI run "Hello" --input image:nonexistent.png diff --git a/tests/e2e/cuj_11.sh b/tests/e2e/cuj_11.sh deleted file mode 100755 index 24f6521..0000000 --- a/tests/e2e/cuj_11.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-11: Image generation -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-11: Image generation ===" - -# dry-run -$CLI run "A blue square" --model gemini-3-pro-image-preview --output tmp/test_gen.png --dry-run - -# live -$CLI run "Generate a simple blue square" --model gemini-3-pro-image-preview --output tmp/test_gen.png diff --git a/tests/e2e/cuj_12.sh b/tests/e2e/cuj_12.sh deleted file mode 100755 index 7046812..0000000 --- a/tests/e2e/cuj_12.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-12: Image generation with config (aspect ratio, size) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-12: Image generation with config (aspect ratio, size) ===" - -# dry-run -$CLI run "A sunset" --model gemini-3-pro-image-preview --aspect-ratio 16:9 --image-size 2k --dry-run - -# live -$CLI run "A sunset over mountains" --model gemini-3-pro-image-preview --aspect-ratio 16:9 --image-size 2k --output tmp/test_sunset.png diff --git a/tests/e2e/cuj_13.sh b/tests/e2e/cuj_13.sh deleted file mode 100755 index 629e488..0000000 --- a/tests/e2e/cuj_13.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-13: Text-to-speech (TTS) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-13: Text-to-speech (TTS) ===" - -# dry-run -$CLI run "Hello world" --model gemini-3.1-flash-tts-preview --voice Kore --language en-US --output tmp/test_tts.wav --dry-run - -# live -$CLI run "Hello world" --model gemini-3.1-flash-tts-preview --voice Kore --output tmp/test_tts.wav diff --git a/tests/e2e/cuj_14.sh b/tests/e2e/cuj_14.sh deleted file mode 100755 index 1429a60..0000000 --- a/tests/e2e/cuj_14.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-14: Image editing (input image + output image) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-14: Image editing (input image + output image) ===" - -# Create test image -echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" | base64 -d > tmp/test_edit.png - -# dry-run -$CLI run "Make it green" --input image:tmp/test_edit.png --response-modality image --model gemini-3-pro-image-preview --output tmp/test_edited.png --dry-run - -# live -$CLI run "Make this image green" --input image:tmp/test_edit.png --response-modality image --model gemini-3-pro-image-preview --output tmp/test_edited.png diff --git a/tests/e2e/cuj_15.sh b/tests/e2e/cuj_15.sh deleted file mode 100755 index c41db45..0000000 --- a/tests/e2e/cuj_15.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-15: Image editing with edit_strength and mask -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-15: Image editing with edit_strength and mask ===" - -# dry-run -echo "dummy" > tmp/tmp_input.png -echo "dummy" > tmp/tmp_mask.png -$CLI run "Edit this" --input image:tmp/tmp_input.png --response-modality image --edit-strength 0.5 --mask tmp/tmp_mask.png --dry-run diff --git a/tests/e2e/cuj_16.sh b/tests/e2e/cuj_16.sh deleted file mode 100755 index 0b8af0e..0000000 --- a/tests/e2e/cuj_16.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-16: Code execution tool -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-16: Code execution tool ===" - -# dry-run -$CLI run "Calculate 2+2" --tool code_execution --dry-run - -# live -$CLI run "Use code execution to calculate 2+2 and return only the number" --tool code_execution diff --git a/tests/e2e/cuj_17.sh b/tests/e2e/cuj_17.sh deleted file mode 100755 index c77aa04..0000000 --- a/tests/e2e/cuj_17.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-17: Google Search tool -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-17: Google Search tool ===" - -# dry-run -$CLI run "What happened today?" --tool google_search --dry-run - -# live -$CLI run "What is the current population of Tokyo? Use search." --tool google_search diff --git a/tests/e2e/cuj_18.sh b/tests/e2e/cuj_18.sh deleted file mode 100755 index febd914..0000000 --- a/tests/e2e/cuj_18.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-18: URL context tool -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-18: URL context tool ===" - -# dry-run -$CLI run "Summarize https://www.wikipedia.org/" --tool url_context --dry-run - -# live -$CLI run "Summarize the content of https://www.wikipedia.org/" --tool url_context diff --git a/tests/e2e/cuj_19.sh b/tests/e2e/cuj_19.sh deleted file mode 100755 index 45e24c7..0000000 --- a/tests/e2e/cuj_19.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-19: Multiple tools together -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-19: Multiple tools together ===" - -# dry-run -$CLI run "Search and calculate" --tool google_search --tool code_execution --dry-run - -# live -$CLI run "Search for the GDP of France then calculate GDP per capita" --tool google_search --tool code_execution diff --git a/tests/e2e/cuj_20.sh b/tests/e2e/cuj_20.sh deleted file mode 100755 index b9ed172..0000000 --- a/tests/e2e/cuj_20.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-20: Invalid tool error -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-20: Invalid tool error ===" - -! $CLI run "Hello" --tool invalid_tool diff --git a/tests/e2e/cuj_21.sh b/tests/e2e/cuj_21.sh deleted file mode 100755 index bd1b05d..0000000 --- a/tests/e2e/cuj_21.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-21: Agent init (scaffold) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-21: Agent init (scaffold) ===" - -AGENT_NAME="e2e-test-agent-$(date +%s)" - -$CLI agents init "$AGENT_NAME" diff --git a/tests/e2e/cuj_22.sh b/tests/e2e/cuj_22.sh deleted file mode 100755 index 1eaaa36..0000000 --- a/tests/e2e/cuj_22.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-22: Agent init is idempotent -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-22: Agent init is idempotent ===" - -AGENT_NAME="e2e-idempotent-$(date +%s)" -$CLI agents init "$AGENT_NAME" -$CLI agents init "$AGENT_NAME" -rm -rf "$AGENT_NAME" diff --git a/tests/e2e/cuj_23.sh b/tests/e2e/cuj_23.sh deleted file mode 100755 index f6a2028..0000000 --- a/tests/e2e/cuj_23.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-23: Agent create (deploy) — dry-run -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-23: Agent create (deploy) — dry-run ===" - -AGENT_NAME="e2e-create-dry-$(date +%s)" -$CLI agents init "$AGENT_NAME" -$CLI agents create --path "./$AGENT_NAME" --dry-run -rm -rf "$AGENT_NAME" diff --git a/tests/e2e/cuj_24.sh b/tests/e2e/cuj_24.sh deleted file mode 100755 index e3ce7fb..0000000 --- a/tests/e2e/cuj_24.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-24: Agent create (deploy) — live -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-24: Agent create (deploy) — live ===" - -AGENT_NAME="e2e-create-live-$(date +%s)" -$CLI agents init "$AGENT_NAME" -$CLI agents create --path "./$AGENT_NAME" -# Cleanup -$CLI agents delete "$AGENT_NAME" --force -rm -rf "$AGENT_NAME" diff --git a/tests/e2e/cuj_25.sh b/tests/e2e/cuj_25.sh deleted file mode 100755 index fbaef5e..0000000 --- a/tests/e2e/cuj_25.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-25: Agent list -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-25: Agent list ===" - -# dry-run -$CLI agents list --dry-run - -# live -$CLI agents list diff --git a/tests/e2e/cuj_26.sh b/tests/e2e/cuj_26.sh deleted file mode 100755 index fc05478..0000000 --- a/tests/e2e/cuj_26.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-26: Agent list (JSON mode) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-26: Agent list (JSON mode) ===" - -# dry-run -$CLI agents list --json --dry-run - -# live -$CLI agents list --json diff --git a/tests/e2e/cuj_27.sh b/tests/e2e/cuj_27.sh deleted file mode 100755 index 61bc350..0000000 --- a/tests/e2e/cuj_27.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-27: Agent get -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-27: Agent get ===" - -# dry-run -$CLI agents get my-agent --dry-run - -# live -$CLI agents get "$AGENT_NAME" diff --git a/tests/e2e/cuj_28.sh b/tests/e2e/cuj_28.sh deleted file mode 100755 index 7cb29e1..0000000 --- a/tests/e2e/cuj_28.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-28: Agent delete -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-28: Agent delete ===" - -# dry-run -$CLI agents delete my-agent --force --dry-run - -# live -AGENT_NAME="e2e-delete-target-$(date +%s)" -$CLI agents init "$AGENT_NAME" -$CLI agents create --path "./$AGENT_NAME" -$CLI agents delete "$AGENT_NAME" --force -rm -rf "$AGENT_NAME" diff --git a/tests/e2e/cuj_29.sh b/tests/e2e/cuj_29.sh deleted file mode 100755 index 55e209a..0000000 --- a/tests/e2e/cuj_29.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-29: Agent full lifecycle (create → list → get → delete) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-29: Agent full lifecycle (create → list → get → delete) ===" - -AGENT_NAME="e2e-lifecycle-$(date +%s)" - -# 1. Init -$CLI agents init "$AGENT_NAME" - -# 2. Create -$CLI agents create --path "./$AGENT_NAME" - -# 3. Test Interaction -$CLI run "What is 2+2?" --agent "$AGENT_NAME" - -# 4. List — should include the agent -$CLI agents list - -# 5. Get — should return details -$CLI agents get "$AGENT_NAME" - -# 6. Delete -$CLI agents delete "$AGENT_NAME" --force - -# 7. Cleanup -rm -rf "$AGENT_NAME" diff --git a/tests/e2e/cuj_30.sh b/tests/e2e/cuj_30.sh deleted file mode 100755 index 5161e0a..0000000 --- a/tests/e2e/cuj_30.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-30: Agent test (interaction via local config) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-30: Agent test (interaction via local config) ===" - -AGENT_NAME="e2e-test-$(date +%s)" -$CLI agents init "$AGENT_NAME" - -# dry-run -$CLI agents test --prompt "Hello" --path "./$AGENT_NAME" --dry-run - -# live -$CLI agents test --prompt "Hello" --path "./$AGENT_NAME" - -rm -rf "$AGENT_NAME" diff --git a/tests/e2e/cuj_31.sh b/tests/e2e/cuj_31.sh deleted file mode 100755 index 558dd5d..0000000 --- a/tests/e2e/cuj_31.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-31: Run with deployed agent (antigravity-preview-05-2026) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-31: Run with deployed agent (antigravity-preview-05-2026) ===" - -# dry-run -$CLI run "What is 2+2?" --agent antigravity-preview-05-2026 --dry-run - -# live -$CLI run "What is 2+2?" --agent antigravity-preview-05-2026 diff --git a/tests/e2e/cuj_32.sh b/tests/e2e/cuj_32.sh deleted file mode 100755 index 7a9dc1d..0000000 --- a/tests/e2e/cuj_32.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-32: Run with custom agent -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-32: Run with custom agent ===" - -# dry-run -$CLI run "Hello" --agent my-custom-agent --dry-run - -# live (requires agent to exist) -$CLI run "Hello" --agent "$AGENT_NAME" diff --git a/tests/e2e/cuj_33.sh b/tests/e2e/cuj_33.sh deleted file mode 100755 index 2a324c2..0000000 --- a/tests/e2e/cuj_33.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-33: Agent with environment persistence (multi-turn) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-33: Agent with environment persistence (multi-turn) ===" - -# dry-run -$CLI run "Continue" --agent antigravity-preview-05-2026 --previous-interaction-id fake_int --dry-run - -# live (two-step) -RESULT=$($CLI run "Write 'hello' to tmp/test.txt" --agent antigravity-preview-05-2026 --json 2>&1) -INT_ID=$(echo "$RESULT" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) -ENV_ID=$(echo "$RESULT" | grep -o '"environment_id":"[^"]*"' | head -1 | cut -d'"' -f4) - -$CLI run "Read tmp/test.txt and tell me what it says" --agent antigravity-preview-05-2026 --previous-interaction-id "$INT_ID" --environment "$ENV_ID" diff --git a/tests/e2e/cuj_34.sh b/tests/e2e/cuj_34.sh deleted file mode 100755 index 3e9141a..0000000 --- a/tests/e2e/cuj_34.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-34: Deep Research (background mode) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-34: Deep Research (background mode) ===" - -# dry-run -$CLI run "Research the history of TPUs" --agent deep-research-preview-04-2026 --dry-run - -# live (will take minutes) -$CLI run "Research the history of Google TPUs in 2 paragraphs" --agent deep-research-preview-04-2026 diff --git a/tests/e2e/cuj_36.sh b/tests/e2e/cuj_36.sh deleted file mode 100755 index 1ceb0e8..0000000 --- a/tests/e2e/cuj_36.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-36: Download environment files (snapshot) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-36: Download environment files ===" - -# dry-run -$CLI files download env_fake123 --dry-run - -# live -echo "Creating test file in environment..." -RESULT=$($CLI run "Write 'hello world' to test.txt" --agent antigravity-preview-05-2026 --json 2>&1) -ENV_ID=$(echo "$RESULT" | grep -o '"environment_id":"[^"]*"' | head -1 | cut -d'"' -f4) - -if [ -z "$ENV_ID" ]; then - # Try fallback to search in full output if json parsing failed or format differs - ENV_ID=$(echo "$RESULT" | grep -o 'environment_id: [a-zA-Z0-9-]*' | head -1 | cut -d' ' -f2) -fi - -if [ -z "$ENV_ID" ]; then - echo "Failed to get environment ID." - echo "Result was:" - echo "$RESULT" - exit 1 -fi - -echo "Found Environment ID: $ENV_ID" - -echo "Downloading snapshot for environment $ENV_ID..." -$CLI files download "$ENV_ID" --output ./tmp - -echo "Verifying extracted files..." -if [ -f "./tmp/snapshot_$ENV_ID/test.txt" ]; then - echo "✓ Verification successful: test.txt found." - rm -rf "./tmp/snapshot_$ENV_ID" -else - echo "✗ Verification failed: test.txt not found." - exit 1 -fi diff --git a/tests/e2e/cuj_37.sh b/tests/e2e/cuj_37.sh deleted file mode 100755 index 5ac6dba..0000000 --- a/tests/e2e/cuj_37.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-37: Missing prompt -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-37: Missing prompt ===" - -! $CLI run diff --git a/tests/e2e/cuj_38.sh b/tests/e2e/cuj_38.sh deleted file mode 100755 index 1987930..0000000 --- a/tests/e2e/cuj_38.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-38: Missing API key -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-38: Missing API key ===" - -! GEMINI_API_KEY="" GEMINI_AUTOPUSH_API_KEY="" $CLI run "Hello" diff --git a/tests/e2e/cuj_39.sh b/tests/e2e/cuj_39.sh deleted file mode 100755 index 0d46b40..0000000 --- a/tests/e2e/cuj_39.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-39: Invalid model (400 error) -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-39: Invalid model (400 error) ===" - -! $CLI run "Hello" --model nonexistent-model diff --git a/tests/e2e/cuj_40.sh b/tests/e2e/cuj_40.sh deleted file mode 100755 index e6e9b84..0000000 --- a/tests/e2e/cuj_40.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-40: Missing agent.yaml for agents create -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-40: Missing agent.yaml for agents create ===" - -! $CLI agents create --path tmp/empty-dir-that-does-not-exist diff --git a/tests/e2e/cuj_41.sh b/tests/e2e/cuj_41.sh deleted file mode 100755 index 72cf4fb..0000000 --- a/tests/e2e/cuj_41.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-41: Dry-run on all commands -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-41: Dry-run on all commands ===" - -$CLI run "Hello" --dry-run -$CLI agents create --dry-run -$CLI agents list --dry-run -$CLI agents get my-agent --dry-run -$CLI agents delete my-agent --force --dry-run -$CLI files list env_123 --dry-run -$CLI files download env_123 --dry-run diff --git a/tests/e2e/cuj_42.sh b/tests/e2e/cuj_42.sh deleted file mode 100755 index 2ea8695..0000000 --- a/tests/e2e/cuj_42.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-42: Completion summary metadata -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-42: Completion summary metadata ===" - -$CLI run "Say hello" diff --git a/tests/e2e/cuj_43.sh b/tests/e2e/cuj_43.sh deleted file mode 100755 index 9895cb2..0000000 --- a/tests/e2e/cuj_43.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-43: Interaction logging -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-43: Interaction logging ===" - -rm -rf .gemini/logs -$CLI run "Hello" -ls .gemini/logs/ diff --git a/tests/e2e/cuj_44.sh b/tests/e2e/cuj_44.sh deleted file mode 100644 index 19a3d9b..0000000 --- a/tests/e2e/cuj_44.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CUJ-44: Invalid agent error -source ~/.bash_profile -CLI="bun run src/cli.ts" -mkdir -p tmp - -echo "=== Running CUJ-44: Invalid agent error ===" - -! $CLI run "Hello" --agent invalid_agent diff --git a/tests/e2e/cuj_45.sh b/tests/e2e/cuj_45.sh deleted file mode 100755 index 26012f0..0000000 --- a/tests/e2e/cuj_45.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# CUJ-45: Agent test with network transform (dry-run) -source ~/.bash_profile -CLI="bun run src/cli.ts" - -AGENT_NAME="cuj-45-agent-$(date +%s)" - -echo "=== Running CUJ-45: Agent test with network transform (dry-run) ===" - -# 1. Initialize agent project -$CLI agents init "$AGENT_NAME" > /dev/null - -# 2. Inject custom agent.yaml with network transforms -cat <<EOF > "$AGENT_NAME/agent.yaml" -id: $AGENT_NAME -base_agent: antigravity-preview-05-2026 -tools: - - type: code_execution -environment: - type: "remote" - sources: - - type: "gcs" - source: "gs://test-bucket/data" - target: ".agents/workspace" - network: - allowlist: - - domain: "api.github.com" - transform: - Authorization: "Bearer ghp_secret_oauth_token" - - domain: "*" -EOF - -# 3. Dry-run and capture curl output -OUTPUT=$($CLI agents test --prompt "Hello" --path "./$AGENT_NAME" --dry-run 2>&1) - -# 4. Assert that network transforms are correctly preserved and printed in the dry-run payload -if echo "$OUTPUT" | grep -q "ghp_secret_oauth_token"; then - echo "✓ SUCCESS: Network transform header was found in dry-run request payload!" -else - echo "✗ FAILED: Network transform header was missing from dry-run request payload!" - echo "Output:" - echo "$OUTPUT" - rm -rf "$AGENT_NAME" - exit 1 -fi - -# 5. Cleanup -rm -rf "$AGENT_NAME" -echo "✓ Cleanup complete." diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh deleted file mode 100755 index aef1257..0000000 --- a/tests/e2e/run_all.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -source ~/.bash_profile -echo "Running all E2E tests..." -for f in tests/e2e/cuj_*.sh; do - echo "----------------------------------------" - echo "Running $f" - echo "----------------------------------------" - bash "$f" - if [ $? -ne 0 ]; then - echo "FAILED: $f" - fi -done diff --git a/tests/files.test.ts b/tests/files.test.ts deleted file mode 100644 index 473eb20..0000000 --- a/tests/files.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as path from "node:path"; - -describe("files (live API)", () => { - test("download environment snapshot", async () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - - // 1. Create a file in the environment - const runCmd = `source ~/.bash_profile && bun run src/cli.ts run "Write 'hello world' to /tmp/test.txt" --agent antigravity-preview-05-2026 2>&1`; - let output = ""; - try { - output = execSync(runCmd, { encoding: "utf-8", shell: "/bin/bash" }); - } catch (e: any) { - output = e.stdout || e.stderr || ""; - } - - console.log("Run output:", output); - - // 2. Extract environment_id - const envIdMatch = output.match(/environment_id:\s*([a-zA-Z0-9-]+)/); - if (!envIdMatch) { - throw new Error("Failed to extract environment_id from output"); - } - const envId = envIdMatch[1]; - console.log(`Found environment_id: ${envId}`); - - // 3. Download the snapshot - const downloadCmd = `source ~/.bash_profile && bun run src/cli.ts files download ${envId} --output ./tmp 2>&1`; - let downloadOutput = ""; - try { - downloadOutput = execSync(downloadCmd, { encoding: "utf-8", shell: "/bin/bash" }); - } catch (e: any) { - downloadOutput = e.stdout || e.stderr || ""; - } - - console.log("Download output:", downloadOutput); - - // 4. Verify - // The user wants us to run it. If it fails with 404, the test will fail, - // which is what we expect if the endpoint is broken as the comment said. - // But let's assert what we expect on success. - expect(downloadOutput).toContain("Saved snapshot"); - - // Cleanup - const snapshotDir = path.join("./tmp", `snapshot_${envId}`); - if (fs.existsSync(snapshotDir)) { - fs.rmSync(snapshotDir, { recursive: true, force: true }); - } - }, 60000); // Give it 60 seconds as it involves interaction + download -}); diff --git a/tests/fixtures/agent-configs/empty/agent.yaml b/tests/fixtures/agent-configs/empty/agent.yaml deleted file mode 100644 index 932b798..0000000 --- a/tests/fixtures/agent-configs/empty/agent.yaml +++ /dev/null @@ -1 +0,0 @@ -# Empty file diff --git a/tests/fixtures/agent-configs/invalid/agent.yaml b/tests/fixtures/agent-configs/invalid/agent.yaml deleted file mode 100644 index ddf7dfe..0000000 --- a/tests/fixtures/agent-configs/invalid/agent.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Missing required 'id' field -base_agent: antigravity-preview-05-2026 -tools: - - type: invalid_tool_type diff --git a/tests/fixtures/agent-configs/malformed/agent.yaml b/tests/fixtures/agent-configs/malformed/agent.yaml deleted file mode 100644 index 022d2b1..0000000 --- a/tests/fixtures/agent-configs/malformed/agent.yaml +++ /dev/null @@ -1 +0,0 @@ -id: [ diff --git a/tests/fixtures/agent-configs/valid/agent.yaml b/tests/fixtures/agent-configs/valid/agent.yaml deleted file mode 100644 index 643303a..0000000 --- a/tests/fixtures/agent-configs/valid/agent.yaml +++ /dev/null @@ -1,3 +0,0 @@ -id: test-agent -base_agent: antigravity-preview-05-2026 -description: Valid test agent diff --git a/tests/fixtures/agent-configs/with-env-vars/.env b/tests/fixtures/agent-configs/with-env-vars/.env deleted file mode 100644 index 187ec8d..0000000 --- a/tests/fixtures/agent-configs/with-env-vars/.env +++ /dev/null @@ -1,3 +0,0 @@ -GITHUB_TOKEN=env-file-github-token -GITHUB_PAT=env-file-github-pat -GEMINI_API_KEY=env-file-gemini-api-key diff --git a/tests/fixtures/agent-configs/with-env-vars/agent.yaml b/tests/fixtures/agent-configs/with-env-vars/agent.yaml deleted file mode 100644 index 93fa030..0000000 --- a/tests/fixtures/agent-configs/with-env-vars/agent.yaml +++ /dev/null @@ -1,19 +0,0 @@ -id: test-agent-with-env-vars -base_agent: antigravity-preview-05-2026 -description: Agent with environment variable placeholders -sources: - - type: github - source: "https://${GITHUB_PAT}@github.com/my-org/private-repo" - target: "/workspace/private-repo" -environment: - type: remote - network: - allowlist: - - domain: api.github.com - transform: - Authorization: "Bearer ${GITHUB_TOKEN}" - X-GitHub-Token: "${GITHUB_TOKEN}" - - domain: generativelanguage.googleapis.com - transform: - x-goog-api-key: "${GEMINI_API_KEY}" - Authorization: "Bearer ${GEMINI_API_KEY}" diff --git a/tests/fixtures/agent-configs/with-examples/agent.yaml b/tests/fixtures/agent-configs/with-examples/agent.yaml deleted file mode 100644 index c06ac66..0000000 --- a/tests/fixtures/agent-configs/with-examples/agent.yaml +++ /dev/null @@ -1,8 +0,0 @@ -id: test-agent-examples -base_agent: antigravity-preview-05-2026 -description: Agent with examples -examples: - - title: "Write a poem" - prompt: "Write a short poem about coding" - - title: "Explain AI" - prompt: "Explain artificial intelligence in simple terms" diff --git a/tests/fixtures/agent-configs/with-tools/agent.yaml b/tests/fixtures/agent-configs/with-tools/agent.yaml deleted file mode 100644 index 556c80c..0000000 --- a/tests/fixtures/agent-configs/with-tools/agent.yaml +++ /dev/null @@ -1,6 +0,0 @@ -id: test-agent-with-tools -base_agent: antigravity-preview-05-2026 -description: Agent with tools -tools: - - type: google_search - - type: code_execution diff --git a/tests/fixtures/inputs/test.png b/tests/fixtures/inputs/test.png deleted file mode 100644 index 51be2f5..0000000 Binary files a/tests/fixtures/inputs/test.png and /dev/null differ diff --git a/tests/help.test.ts b/tests/help.test.ts deleted file mode 100644 index 80b7997..0000000 --- a/tests/help.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; - -// Skip: citty uses consola for --help output, which suppresses stdout -// when not connected to a TTY. bun test's execSync doesn't provide a TTY, -// so help output is always empty. Help works correctly when run directly -// (e.g. `bun run dev --help`). -describe("help", () => { - const runCli = (args: string) => { - const cmd = `source ~/.bash_profile && CONSOLA_LEVEL=5 bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8", shell: "/bin/bash" }); - } catch (e: any) { - return (e.stdout || "") + (e.stderr || ""); - } - }; - - test("--help shows top-level help", () => { - const output = runCli("--help"); - expect(output).toContain("gemini-api"); - expect(output).toContain("COMMANDS"); - }); - - test("run --help shows examples", () => { - const output = runCli("run --help"); - expect(output).toContain("Examples"); - expect(output).toContain("gemini-api run"); - }); - - test("agents --help shows subcommands", () => { - const output = runCli("agents --help"); - expect(output).toContain("COMMANDS"); - expect(output).toContain("create"); - }); - - test("agents create --help shows examples", () => { - const output = runCli("agents create --help"); - expect(output).toContain("Examples"); - expect(output).toContain("gemini-api agents create"); - }); -}); diff --git a/tests/helpers.ts b/tests/helpers.ts deleted file mode 100644 index bab9ed3..0000000 --- a/tests/helpers.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { type ExecSyncOptions, execSync } from "node:child_process"; - -const CLI = "bun run src/cli.ts"; - -export function run(args: string, opts?: ExecSyncOptions): string { - return execSync(`${CLI} ${args}`, { - encoding: "utf-8", - timeout: 120_000, // 2 min timeout for API calls - ...opts, - }); -} - -export function runJson(args: string): any[] { - const result = run(`${args} --json`); - return result - .trim() - .split("\n") - .map((l) => JSON.parse(l)); -} - -export function runExpectError(args: string): string { - try { - execSync(`${CLI} ${args}`, { encoding: "utf-8", stdio: "pipe" }); - throw new Error("Expected command to fail"); - } catch (e: any) { - return e.stderr || e.stdout || e.message; - } -} - -// Generate a 1x1 red pixel PNG for image tests -export function createTestPng(): Buffer { - // Minimal valid PNG: 1x1 red pixel - return Buffer.from( - "89504e470d0a1a0a0000000d49484452000000010000000108020000009001" + - "2e00000000c4944415408d763f8cfc0f0030001012718e3600000000049454e44ae426082", - "hex", - ); -} diff --git a/tests/logging.test.ts b/tests/logging.test.ts deleted file mode 100644 index 6676ee2..0000000 --- a/tests/logging.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; -import * as fs from "node:fs"; -import { join } from "node:path"; - -describe("interaction logging", () => { - const logDir = join(process.cwd(), ".gemini", "logs"); - - beforeAll(() => { - fs.rmSync(logDir, { recursive: true, force: true }); - }); - - test("run creates log file", () => { - const result = execSync('source ~/.bash_profile && bun run src/cli.ts run "Say hello" 2>&1', { - encoding: "utf-8", - shell: "/bin/bash", - }); - - const match = result.match(/interaction_id: ([^\s]+)/); - const intId = match ? match[1] : null; - expect(intId).toBeTruthy(); - - const logFile = join(logDir, `${intId}.jsonl`); - expect(fs.existsSync(logFile)).toBe(true); - - const lines = fs.readFileSync(logFile, "utf-8").trim().split("\n"); - expect(lines.length).toBe(2); - - const request = JSON.parse(lines[0]); - expect(request.type).toBe("request"); - expect(request.data.input).toBeDefined(); - - const response = JSON.parse(lines[1]); - expect(response.type).toBe("response"); - expect(response.data.outputs).toBeDefined(); - // Note: usage may be at data.usage or data.interaction.usage depending - // on the server response format. If undefined, it's a known API inconsistency - // (see FINDINGS.md §3). - if (response.data.usage) { - expect(response.data.usage.inputTokens).toBeDefined(); - } - }, 60000); - - test("dry-run does NOT create log", () => { - const before = fs.existsSync(logDir) ? fs.readdirSync(logDir).length : 0; - execSync('source ~/.bash_profile && bun run src/cli.ts run "Hello" --dry-run --api-key fake', { - encoding: "utf-8", - shell: "/bin/bash", - }); - const after = fs.existsSync(logDir) ? fs.readdirSync(logDir).length : 0; - expect(after).toBe(before); - }); - - test("log does not include binary data", () => { - fs.mkdirSync("tmp", { recursive: true }); - - const result = execSync( - 'source ~/.bash_profile && bun run src/cli.ts run "Hello world" --model gemini-3.1-flash-tts-preview --voice Kore --output ./tmp/test.wav 2>&1', - { encoding: "utf-8", shell: "/bin/bash" }, - ); - - const match = result.match(/interaction_id: ([^\s]+)/); - const intId = match ? match[1] : null; - expect(intId).toBeTruthy(); - - const logFile = join(logDir, `${intId}.jsonl`); - expect(fs.existsSync(logFile)).toBe(true); - - const lastLog = fs.readFileSync(logFile, "utf-8"); - - // Should not contain base64 audio data - expect(lastLog.length).toBeLessThan(10000); - }, 60000); - - afterAll(() => { - fs.rmSync("tmp", { recursive: true, force: true }); - }); -}); diff --git a/tests/output.test.ts b/tests/output.test.ts deleted file mode 100644 index 61f3615..0000000 --- a/tests/output.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { HumanStreamRenderer, printCompletionSummary } from "../src/lib/output"; -import type { StreamResult } from "../src/lib/stream"; - -describe("HumanStreamRenderer Normal Mode (Concise)", () => { - test("renders thought step as [thought] line", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, false); - - renderer.handleStepStart({ - type: "step.start", - data: { index: 0, step: { type: "thought" } }, - raw: "", - }); - expect(output).toBe("[thought]\n"); - }); - - test("buffers tool call and outputs combined single line on result", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, false); - - // 1. Function Call step starts, deltas arrive, and stops - renderer.handleStepStart({ - type: "step.start", - data: { index: 1, step: { type: "function_call", name: "write_file" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { - index: 1, - delta: { name: "write_file", arguments: '{"path":"/hello.py","content":"print(1)"}' }, - }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 1 }, - raw: "", - }); - - // Output should still be empty because tool call is buffered - expect(output).toBe(""); - - // 2. Function Result step starts, deltas arrive, and stops - renderer.handleStepStart({ - type: "step.start", - data: { index: 2, step: { type: "function_result", name: "write_file" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 2, delta: { result: '{"success":true}' } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 2 }, - raw: "", - }); - - expect(output).toBe('[tool] write_file(path="/hello.py") -> {"success":true}\n'); - }); - - test("buffers code execution and outputs combined single line on result", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, false); - - // 1. Code execution call starts and stops - renderer.handleStepStart({ - type: "step.start", - data: { index: 1, step: { type: "code_execution_call" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 1, delta: { arguments: { code: "print(2 + 2)\n" } } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 1 }, - raw: "", - }); - - expect(output).toBe(""); - - // 2. Code execution result starts and stops - renderer.handleStepStart({ - type: "step.start", - data: { index: 2, step: { type: "code_execution_result" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 2, delta: { result: "4\n", is_error: false } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 2 }, - raw: "", - }); - - expect(output).toBe('[code] print(2 + 2) -> "4"\n'); - }); - - test("renders model text output directly without padding", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, false); - - renderer.handleStepStart({ - type: "step.start", - data: { index: 1, step: { type: "model_output" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 1, delta: { text: "Hello\nworld" } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 1 }, - raw: "", - }); - - expect(output).toBe("[text]\nHello\nworld"); - }); - - test("does not render [text] header if there is no text output (e.g. media-only output)", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, false); - - renderer.handleStepStart({ - type: "step.start", - data: { index: 1, step: { type: "model_output" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 1, delta: { data: "base64bytes...", mime_type: "image/png" } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 1 }, - raw: "", - }); - - expect(output).toBe(""); - }); - - test("uses step type from block if step type was unknown at start", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, false); - - // 1. Code execution call starts with unknown type, but resolved in delta - renderer.handleStepStart({ - type: "step.start", - data: { index: 1 }, - raw: "", - }); - renderer.handleStepDelta( - { - type: "step.delta", - data: { index: 1, delta: { arguments: { code: "print(2 + 2)\n" } } }, - raw: "", - }, - { - type: "code_execution_call", - arguments: { code: "print(2 + 2)\n" }, - id: "call_1", - } - ); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 1 }, - raw: "", - }); - - expect(output).toBe(""); - - // 2. Code execution result starts and stops - renderer.handleStepStart({ - type: "step.start", - data: { index: 2, step: { type: "code_execution_result" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 2, delta: { result: "4\n", is_error: false } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 2 }, - raw: "", - }); - - expect(output).toBe('[code] print(2 + 2) -> "4"\n'); - }); -}); - -describe("HumanStreamRenderer Verbose Mode", () => { - test("prints completed step as a single JSON line", () => { - let output = ""; - const mockStdout = { - write(data: string) { - output += data; - return true; - }, - } as typeof process.stdout; - - const renderer = new HumanStreamRenderer(mockStdout, true); - - renderer.handleStepStart({ - type: "step.start", - data: { index: 0, step: { type: "thought" } }, - raw: "", - }); - renderer.handleStepDelta({ - type: "step.delta", - data: { index: 0, delta: { signature: "EvQBC..." } }, - raw: "", - }); - renderer.handleStepStop({ - type: "step.stop", - data: { index: 0 }, - raw: "", - }); - - const parsed = JSON.parse(output.trim()); - expect(parsed.index).toBe(0); - expect(parsed.type).toBe("thought"); - expect(parsed.status).toBe("completed"); - expect(parsed.thought.signature).toBe("EvQBC..."); - }); - - test("prints verbose completion summary as interaction JSON", () => { - let consoleOutput = ""; - const originalLog = console.log; - console.log = (msg) => { - consoleOutput += msg; - }; - - try { - const mockResult: StreamResult = { - interactionId: "v1_test123", - status: "completed", - environmentId: "env_abc", - outputs: [], - steps: [], - usage: { - inputTokens: 100, - outputTokens: 50, - thoughtTokens: 25, - cachedTokens: 10, - }, - }; - - printCompletionSummary(mockResult, 5.5, true); - - const parsed = JSON.parse(consoleOutput.trim()); - expect(parsed.interaction).toBeDefined(); - expect(parsed.interaction.id).toBe("v1_test123"); - expect(parsed.interaction.environment_id).toBe("env_abc"); - expect(parsed.interaction.status).toBe("completed"); - expect(parsed.interaction.usage.total_tokens).toBe(150); - expect(parsed.interaction.usage.total_input_tokens).toBe(100); - expect(parsed.interaction.usage.total_output_tokens).toBe(50); - expect(parsed.interaction.usage.total_thought_tokens).toBe(25); - expect(parsed.interaction.usage.total_cached_tokens).toBe(10); - } finally { - console.log = originalLog; - } - }); -}); diff --git a/tests/run-multimodal.test.ts b/tests/run-multimodal.test.ts deleted file mode 100644 index 53c4734..0000000 --- a/tests/run-multimodal.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; -import { existsSync, statSync, unlinkSync, writeFileSync } from "node:fs"; - -describe("multimodal input (live API)", () => { - const runCli = (args: string, input?: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - const options: any = { encoding: "utf-8" }; - if (input) { - options.input = input; - } - try { - return execSync(cmd, options); - } catch (e: any) { - return e.stdout; - } - }; - - function createTestPng() { - const base64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - return Buffer.from(base64, "base64"); - } - - test("image input is processed", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const png = createTestPng(); - writeFileSync("test_input.png", png); - - const result = runCli('run "What color is this image?" --input image:test_input.png'); - expect(result.toLowerCase()).toMatch(/red|salmon|coral/); - unlinkSync("test_input.png"); - }, 180000); - - test("missing input file errors clearly", () => { - const result = runCli('run "Hello" --input image:nonexistent.png'); - expect(result).toContain("File not found"); - }); -}); - -describe("image generation (live API)", () => { - const runCli = (args: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("image output is saved to file", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - runCli( - 'run "Generate a simple blue square" --model gemini-3-pro-image-preview --output test_output.png', - ); - expect(existsSync("test_output.png")).toBe(true); - expect(statSync("test_output.png").size).toBeGreaterThan(100); - unlinkSync("test_output.png"); - }, 180000); -}); - -describe("TTS (live API)", () => { - const runCli = (args: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("audio output is saved to file", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const result = runCli( - 'run "Hello world" --model gemini-3.1-flash-tts-preview --voice Kore --output test_tts.wav', - ); - if (!existsSync("test_tts.wav")) { - console.error("TTS failed. Output:", result); - } - expect(existsSync("test_tts.wav")).toBe(true); - expect(statSync("test_tts.wav").size).toBeGreaterThan(100); - unlinkSync("test_tts.wav"); - }, 180000); -}); - -describe("image editing (live API)", () => { - const runCli = (args: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("image editing produces output file", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - - // Create a test image - const base64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - const png = Buffer.from(base64, "base64"); - writeFileSync("test_edit_input.png", png); - - try { - const result = runCli( - 'run "Make this image green" --input image:test_edit_input.png --response-modality image --output test_edit_output.png --model gemini-3-pro-image-preview', - ); - - if (!existsSync("test_edit_output.png")) { - console.error("Image editing failed. Output:", result); - } - - expect(existsSync("test_edit_output.png")).toBe(true); - expect(statSync("test_edit_output.png").size).toBeGreaterThan(100); - } finally { - if (existsSync("test_edit_input.png")) unlinkSync("test_edit_input.png"); - if (existsSync("test_edit_output.png")) unlinkSync("test_edit_output.png"); - } - }, 180000); -}); - -describe("image editing (dry-run)", () => { - const runCli = (args: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("image editing flags are mapped to generation_config", () => { - // Create dummy files - writeFileSync("tmp_input.png", "dummy content"); - writeFileSync("tmp_mask.png", "dummy content"); - - try { - const result = runCli( - 'run "Edit this image" --input image:tmp_input.png --response-modality image --edit-strength 0.5 --mask tmp_mask.png --dry-run', - ); - - expect(result).toContain('"edit_strength": 0.5'); - expect(result).toContain('"mask":'); - } finally { - if (existsSync("tmp_input.png")) unlinkSync("tmp_input.png"); - if (existsSync("tmp_mask.png")) unlinkSync("tmp_mask.png"); - } - }); -}); diff --git a/tests/run-tools.test.ts b/tests/run-tools.test.ts deleted file mode 100644 index cb42ada..0000000 --- a/tests/run-tools.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; - -describe("tools (live API)", () => { - const runCli = (args: string) => { - const cmd = `source ~/.bash_profile && bun run src/cli.ts ${args} 2>&1`; - try { - return execSync(cmd, { encoding: "utf-8" }); - } catch (e: any) { - return e.stdout; - } - }; - - test("code_execution works", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const result = runCli( - 'run "Use code execution to calculate 2+2 and return only the number" --tool code_execution', - ); - expect(result).toContain("4"); - expect(result).not.toContain("API error"); - }, 30000); - - test("google_search works", () => { - if (!process.env.GEMINI_API_KEY) return; - const result = runCli( - 'run "What is the current population of Tokyo? Use search." --tool google_search', - ); - expect(result).toContain("✓ completed"); - expect(result).not.toContain("API error"); - }, 30000); - - test("multiple tools", () => { - if (!process.env.GEMINI_API_KEY) return; - const result = runCli( - 'run "Search for the GDP of France then calculate GDP per capita" --tool google_search --tool code_execution', - ); - expect(result).toContain("✓ completed"); - expect(result).not.toContain("API error"); - }, 30000); - - test("invalid tool shows error", () => { - const result = runCli('run "Hello" --tool invalid_tool'); - expect(result).toContain("Unknown tool"); - expect(result).toContain("code_execution"); - }); - - test("dry-run includes tools in curl", () => { - const result = runCli('run "Hello" --tool code_execution --dry-run --api-key fake'); - expect(result).toContain("code_execution"); - }); - - test("dry-run includes complex tools in curl", () => { - const result = runCli( - 'run "Hello" --tool \'mcp_server:weather:https://example.com/mcp\' --tool \'function:get_weather:{"type":"object","properties":{"location":{"type":"string"}}}\' --dry-run --api-key fake', - ); - expect(result).toContain("mcp_server"); - expect(result).toContain("weather"); - expect(result).toContain("https://example.com/mcp"); - expect(result).toContain("function"); - expect(result).toContain("get_weather"); - expect(result).toContain("parameters"); - }); - - test("dry-run includes tool-choice in curl", () => { - const result = runCli( - 'run "Hello" --tool code_execution --tool-choice any --dry-run --api-key fake', - ); - expect(result).toContain("tool_choice"); - expect(result).toContain("any"); - }); -}); diff --git a/tests/run.test.ts b/tests/run.test.ts deleted file mode 100644 index eaa58d9..0000000 --- a/tests/run.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { execSync } from "node:child_process"; - -describe("gemini-api run", () => { - // Helper to run CLI - const runCli = (args: string, input?: string) => { - const cmd = `bun run src/cli.ts ${args} 2>&1`; - const options: any = { encoding: "utf-8" }; - if (input) { - options.input = input; - } - try { - return execSync(cmd, options); - } catch (e: any) { - return e.stdout; // Output is already combined - } - }; - - test("basic text interaction returns response", () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const result = runCli('run "Say exactly: pong"'); - expect(result).toContain("pong"); - expect(result).toContain("interaction_id:"); - }, 30000); - - test("--json outputs JSONL", () => { - if (!process.env.GEMINI_API_KEY) return; - const result = runCli('run "Say hi" --json'); - const lines = result.trim().split("\n"); - const events = lines - .map((l) => { - try { - return JSON.parse(l); - } catch (_e) { - return null; - } - }) - .filter((e) => e !== null); - - expect(events.length).toBeGreaterThan(0); - expect(events[0].event_type).toBeDefined(); - }, 30000); - - test("streaming returns text incrementally", () => { - if (!process.env.GEMINI_API_KEY) return; - const result = runCli('run "Count to 5"'); - expect(result).toContain("1"); - expect(result).toContain("5"); - expect(result).toContain("✓ completed"); - }, 30000); - - test("stdin input works", () => { - if (!process.env.GEMINI_API_KEY) return; - const fs = require("node:fs"); - if (!fs.existsSync("tmp")) fs.mkdirSync("tmp"); - fs.writeFileSync("tmp/tmp_prompt.txt", "Say exactly: stdin-works"); - const cmd = `bun run src/cli.ts run - < tmp/tmp_prompt.txt`; - const result = execSync(cmd, { encoding: "utf-8" }); - expect(result).toContain("stdin-works"); - fs.unlinkSync("tmp/tmp_prompt.txt"); - }, 30000); - - test("multi-turn with previous-interaction-id", () => { - if (!process.env.GEMINI_API_KEY) return; - // First turn - const r1 = runCli('run "Remember the word: banana" --json'); - const lines = r1.trim().split("\n"); - let intId = ""; - for (const line of lines) { - try { - const data = JSON.parse(line); - if (data.interaction?.id) { - intId = data.interaction.id; - break; - } - } catch (_e) { - // Ignore - } - } - - expect(intId).toBeTruthy(); - - // Second turn - const r2 = runCli( - `run "What word did I ask you to remember?" --previous-interaction-id ${intId}`, - ); - expect(r2.toLowerCase()).toContain("banana"); - }, 60000); - - test("missing prompt shows error", () => { - const result = runCli("run"); - expect(result).toContain("Missing prompt"); - }); -}); diff --git a/tests/streaming.test.ts b/tests/streaming.test.ts deleted file mode 100644 index 995fffa..0000000 --- a/tests/streaming.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, expect, test } from "bun:test"; -import { apiStreamRequest, resolveContext } from "../src/lib/api"; -import { processStream, type StreamEvent } from "../src/lib/stream"; - -describe("streaming (live API)", () => { - test("text streaming produces complete result", async () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const ctx = resolveContext({}); - const response = await apiStreamRequest(ctx, "/interactions", { - model: "gemini-3-flash-preview", - input: "Say exactly: streaming-works", - stream: true, - }); - - const events: StreamEvent[] = []; - const result = await processStream(response, { - onEvent: (e) => events.push(e), - onComplete: () => {}, - }); - - // Check events arrived - expect(events.length).toBeGreaterThan(0); - expect(events[0].type).toBe("interaction.created"); - - // Check reassembled result - expect(result.status).toBe("completed"); - expect(result.outputs.length).toBeGreaterThan(0); - const textBlock = result.outputs.find((o) => o.type === "text"); - expect(textBlock).toBeDefined(); - expect((textBlock as any).text).toContain("streaming-works"); - }, 30000); - - test("code execution produces call + result blocks", async () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const ctx = resolveContext({}); - const response = await apiStreamRequest(ctx, "/interactions", { - model: "gemini-3-flash-preview", - input: "Use code execution to calculate 2+2", - tools: [{ type: "code_execution" }], - stream: true, - }); - - const result = await processStream(response, { - onEvent: () => {}, - onComplete: () => {}, - }); - - const codeCall = result.steps.find((s) => s.type === "code_execution_call"); - const codeResult = result.steps.find((s) => s.type === "code_execution_result"); - expect(codeCall).toBeDefined(); - expect(codeResult).toBeDefined(); - }, 60000); - - test("google search produces search call/result", async () => { - if (!process.env.GEMINI_API_KEY) { - console.warn("Skipping live API test because GEMINI_API_KEY is not set"); - return; - } - const ctx = resolveContext({}); - const response = await apiStreamRequest(ctx, "/interactions", { - model: "gemini-3-flash-preview", - input: "What happened in the news today?", - tools: [{ type: "google_search" }], - stream: true, - }); - - const result = await processStream(response, { - onEvent: () => {}, - onComplete: () => {}, - }); - - expect(result.status).toBe("completed"); - expect(result.steps.length).toBeGreaterThan(0); - }, 90000); -}); - -// Mock-data unit tests for step events (no live API required) -describe("step event parsing", () => { - function mockSSEResponse(events: object[]): Response { - const lines = `${events.map((e) => `data: ${JSON.stringify(e)}`).join("\n")}\ndata: [DONE]\n`; - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(lines)); - controller.close(); - }, - }); - return new Response(stream); - } - - test("step.start creates step entry", async () => { - const response = mockSSEResponse([ - { event_type: "interaction.start", interaction: { id: "test-123", status: "in_progress" } }, - { event_type: "step.start", index: 0, step: { type: "thinking", status: "in_progress" } }, - { event_type: "step.stop", index: 0, step: { type: "thinking", status: "completed" } }, - { event_type: "interaction.complete", interaction: { id: "test-123", status: "completed" } }, - ]); - - const events: StreamEvent[] = []; - const result = await processStream(response, { - onEvent: (e) => events.push(e), - onComplete: () => {}, - }); - - expect(result.steps.length).toBe(1); - expect(result.steps[0].type).toBe("thinking"); - expect(result.steps[0].status).toBe("completed"); - expect(events.some((e) => e.type === "step.start")).toBe(true); - expect(events.some((e) => e.type === "step.stop")).toBe(true); - }); - - test("step.delta accumulates text", async () => { - const response = mockSSEResponse([ - { event_type: "interaction.start", interaction: { id: "test-456", status: "in_progress" } }, - { event_type: "step.start", index: 0, step: { type: "tool_use" } }, - { event_type: "step.delta", index: 0, delta: { text: "Searching " } }, - { event_type: "step.delta", index: 0, delta: { text: "the web..." } }, - { event_type: "step.stop", index: 0, step: { type: "tool_use", status: "completed" } }, - { event_type: "interaction.complete", interaction: { id: "test-456", status: "completed" } }, - ]); - - const result = await processStream(response, { - onEvent: () => {}, - onComplete: () => {}, - }); - - expect(result.steps[0].text).toBe("Searching the web..."); - expect(result.steps[0].type).toBe("tool_use"); - }); - - test("multiple steps are tracked independently", async () => { - const response = mockSSEResponse([ - { event_type: "interaction.start", interaction: { id: "test-789", status: "in_progress" } }, - { event_type: "step.start", index: 0, step: { type: "thinking" } }, - { event_type: "step.stop", index: 0, step: { type: "thinking", status: "completed" } }, - { event_type: "step.start", index: 1, step: { type: "tool_use" } }, - { event_type: "step.stop", index: 1, step: { type: "tool_use", status: "completed" } }, - { event_type: "interaction.complete", interaction: { id: "test-789", status: "completed" } }, - ]); - - const result = await processStream(response, { - onEvent: () => {}, - onComplete: () => {}, - }); - - expect(result.steps.length).toBe(2); - expect(result.steps[0].type).toBe("thinking"); - expect(result.steps[1].type).toBe("tool_use"); - }); -}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index c33017d..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "types": ["bun-types"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "tests"] -}