-
Notifications
You must be signed in to change notification settings - Fork 351
Add --gemini-api-target to AWF proxy for Gemini API routing #26060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # ADR-26060: Add Gemini API Target Routing to AWF Proxy | ||
|
|
||
| **Date**: 2026-04-13 | ||
| **Status**: Draft | ||
| **Deciders**: pelikhan, Copilot | ||
|
|
||
| --- | ||
|
|
||
| ## Part 1 — Narrative (Human-Friendly) | ||
|
|
||
| ### Context | ||
|
|
||
| The AWF proxy sidecar provides an LLM gateway that routes API requests from agent containers to external AI providers. For OpenAI (codex), Anthropic (claude), and Copilot engines, the proxy has built-in default routing targets. Gemini was integrated as an engine but never received a corresponding proxy routing target: when a workflow runs with `engine: gemini` and the network firewall enabled, `GEMINI_API_BASE_URL` points at the proxy on port 10003, but the proxy cannot forward the request and returns `API_KEY_INVALID`. The fix must follow the existing pattern for other engines to stay consistent and maintainable. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will add `GetGeminiAPITarget()` to the AWF helpers layer and wire it into `BuildAWFArgs()` so that the `--gemini-api-target` flag is emitted whenever the engine is Gemini. The default target is `generativelanguage.googleapis.com`; when `GEMINI_API_BASE_URL` is set in `engine.env`, the hostname extracted from that URL takes precedence. When the custom URL includes a path component, `--gemini-api-base-path` is also emitted. This mirrors the existing pattern used for `--openai-api-target`, `--anthropic-api-target`, and `--copilot-api-target`, keeping the engine routing model uniform. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Hard-code the Gemini default target inside the AWF sidecar binary | ||
|
|
||
| The AWF sidecar could be patched to know about Gemini's default endpoint without requiring the caller to pass `--gemini-api-target`. This would eliminate the need for the go-layer change. However, it couples the sidecar to a specific vendor endpoint, making it harder to test independently and requiring a sidecar release for every new engine. The current pattern—caller-supplied targets—keeps the sidecar generic. | ||
|
|
||
| #### Alternative 2: Require users to always set `GEMINI_API_BASE_URL` explicitly | ||
|
|
||
| Without a default target, users who want to use the public Gemini endpoint would need to add `GEMINI_API_BASE_URL: "https://generativelanguage.googleapis.com"` to every workflow. This adds boilerplate and differs from every other engine, which all route to a sensible default without extra configuration. The experience asymmetry is a significant usability cost. | ||
|
|
||
| #### Alternative 3: Use `engine.api-target` YAML field instead of an environment variable | ||
|
|
||
| The Copilot engine already has an `engine.api-target` field in the workflow YAML that overrides `GITHUB_COPILOT_BASE_URL`. We could introduce a similar `engine.api-target` for Gemini. However, no other engine besides Copilot uses this field, and adding it only for Gemini would create inconsistency. Using `GEMINI_API_BASE_URL` in `engine.env` aligns Gemini with the codex and claude pattern. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Gemini engine workflows now work correctly when the network firewall is enabled — the proxy can forward requests to the correct upstream. | ||
| - Users get custom endpoint support (`GEMINI_API_BASE_URL`) consistent with the codex and claude engines. | ||
| - The implementation follows the established engine-routing pattern; new engines in the future can be added the same way. | ||
| - `GH_AW_ALLOWED_DOMAINS` is kept in sync with `--allow-domains` via the existing `computeAllowedDomainsForSanitization` hook. | ||
|
|
||
| #### Negative | ||
| - `BuildAWFArgs` grows slightly larger; the engine-specific target logic is co-located in one function rather than being dispatched polymorphically. | ||
| - A hard-coded constant (`DefaultGeminiAPITarget`) must be updated if Google changes the Gemini API hostname, though this is an unlikely scenario. | ||
|
|
||
| #### Neutral | ||
| - The smoke-test lock file (`.github/workflows/smoke-gemini.lock.yml`) must be recompiled to include `--gemini-api-target generativelanguage.googleapis.com` in generated `awf` invocations. | ||
| - Documentation for custom API endpoints in `docs/src/content/docs/reference/engines.md` gains a Gemini example section, extending an existing pattern rather than introducing new concepts. | ||
|
|
||
| --- | ||
|
|
||
| ## Part 2 — Normative Specification (RFC 2119) | ||
|
|
||
| > The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). | ||
|
|
||
| ### Gemini Proxy Target Resolution | ||
|
|
||
| 1. When the active engine is `gemini` and `GEMINI_API_BASE_URL` is not set in `engine.env`, implementations **MUST** emit `--gemini-api-target generativelanguage.googleapis.com` in the `awf` command arguments. | ||
| 2. When `GEMINI_API_BASE_URL` is set in `engine.env`, implementations **MUST** extract the hostname from that URL and emit `--gemini-api-target <hostname>` instead of the default. | ||
| 3. When `GEMINI_API_BASE_URL` contains a non-empty path component (e.g. `/v1/beta`), implementations **MUST** also emit `--gemini-api-base-path <path>`. | ||
| 4. Implementations **MUST NOT** emit `--gemini-api-target` when the engine is not `gemini` and `GEMINI_API_BASE_URL` is not configured. | ||
| 5. The `DefaultGeminiAPITarget` constant **SHOULD** be the single source of truth for the default Gemini hostname; it **MUST NOT** be duplicated as a string literal elsewhere in the codebase. | ||
|
|
||
| ### Domain Allowlist Synchronization | ||
|
|
||
| 1. The effective Gemini API target hostname **MUST** be included in the domain set computed by `computeAllowedDomainsForSanitization()` so that `GH_AW_ALLOWED_DOMAINS` and `--allow-domains` remain consistent. | ||
| 2. Implementations **MUST** call `GetGeminiAPITarget()` with the same `engineID` used for the proxy flag, ensuring both paths resolve identically. | ||
|
|
||
| ### Custom Endpoint Pattern | ||
|
|
||
| 1. New engine API-target integrations **SHOULD** follow the same three-part pattern established here: (a) a `Get<Engine>APITarget()` helper that reads `<ENGINE>_API_BASE_URL` with a default fallback, (b) a call in `BuildAWFArgs()` to emit the `--<engine>-api-target` flag, and (c) inclusion in `computeAllowedDomainsForSanitization()`. | ||
| 2. Engine-specific environment variables for custom endpoints **MUST** follow the naming convention `<ENGINE_UPPERCASE>_API_BASE_URL` (e.g. `GEMINI_API_BASE_URL`, `OPENAI_BASE_URL`). | ||
|
|
||
| ### Conformance | ||
|
|
||
| An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -315,6 +315,21 @@ func BuildAWFArgs(config AWFCommandConfig) []string { | |
| awfHelpersLog.Printf("Added --copilot-api-target=%s", copilotTarget) | ||
| } | ||
|
|
||
| // Add Gemini API target for the LLM gateway proxy. | ||
| // Unlike OpenAI/Anthropic/Copilot where AWF has built-in default routing, | ||
| // Gemini requires an explicit target so the proxy knows where to forward requests. | ||
| // Defaults to generativelanguage.googleapis.com when the engine is Gemini. | ||
| if geminiTarget := GetGeminiAPITarget(config.WorkflowData, config.EngineName); geminiTarget != "" { | ||
| awfArgs = append(awfArgs, "--gemini-api-target", geminiTarget) | ||
| awfHelpersLog.Printf("Added --gemini-api-target=%s", geminiTarget) | ||
| } | ||
|
|
||
| geminiBasePath := extractAPIBasePath(config.WorkflowData, "GEMINI_API_BASE_URL") | ||
| if geminiBasePath != "" { | ||
| awfArgs = append(awfArgs, "--gemini-api-base-path", geminiBasePath) | ||
| awfHelpersLog.Printf("Added --gemini-api-base-path=%s", geminiBasePath) | ||
| } | ||
|
Comment on lines
+318
to
+331
|
||
|
|
||
| // Add SSL Bump support for HTTPS content inspection (v0.9.0+) | ||
| sslBumpArgs := getSSLBumpArgs(firewallConfig) | ||
| awfArgs = append(awfArgs, sslBumpArgs...) | ||
|
|
@@ -491,6 +506,7 @@ func extractAPIBasePath(workflowData *WorkflowData, envVar string) string { | |
| // - Codex: OPENAI_BASE_URL → --openai-api-target | ||
| // - Claude: ANTHROPIC_BASE_URL → --anthropic-api-target | ||
| // - Copilot: GITHUB_COPILOT_BASE_URL → --copilot-api-target (fallback when api-target not set) | ||
| // - Gemini: GEMINI_API_BASE_URL → --gemini-api-target (default: generativelanguage.googleapis.com) | ||
| // | ||
| // Returns empty string if neither source is configured. | ||
| func GetCopilotAPITarget(workflowData *WorkflowData) string { | ||
|
|
@@ -503,6 +519,33 @@ func GetCopilotAPITarget(workflowData *WorkflowData) string { | |
| return extractAPITargetHost(workflowData, "GITHUB_COPILOT_BASE_URL") | ||
| } | ||
|
|
||
| // DefaultGeminiAPITarget is the default Gemini API endpoint hostname. | ||
| // AWF's proxy sidecar needs this target to forward Gemini API requests, since | ||
| // unlike OpenAI/Anthropic/Copilot, the proxy has no built-in default handler for Gemini. | ||
| const DefaultGeminiAPITarget = "generativelanguage.googleapis.com" | ||
|
|
||
| // GetGeminiAPITarget returns the effective Gemini API target hostname for the LLM gateway proxy. | ||
| // Unlike other engines where AWF has built-in default routing, Gemini requires an explicit target. | ||
| // | ||
| // Resolution order: | ||
| // 1. GEMINI_API_BASE_URL in engine.env (custom endpoint) | ||
| // 2. Default: generativelanguage.googleapis.com (when engine is "gemini") | ||
| // | ||
| // Returns empty string if the engine is not Gemini and no custom GEMINI_API_BASE_URL is configured. | ||
| func GetGeminiAPITarget(workflowData *WorkflowData, engineName string) string { | ||
| // Check for custom GEMINI_API_BASE_URL in engine.env | ||
| if customTarget := extractAPITargetHost(workflowData, "GEMINI_API_BASE_URL"); customTarget != "" { | ||
| return customTarget | ||
| } | ||
|
|
||
| // Default to the standard Gemini API endpoint when engine is Gemini | ||
| if engineName == "gemini" { | ||
| return DefaultGeminiAPITarget | ||
| } | ||
|
|
||
| return "" | ||
| } | ||
|
|
||
| // ComputeAWFExcludeEnvVarNames returns the list of environment variable names that must be | ||
| // excluded from the agent container's visible environment via AWF's --exclude-env flag. | ||
| // | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence says "Three environment variables" but then lists four (OPENAI_BASE_URL, ANTHROPIC_BASE_URL, GITHUB_COPILOT_BASE_URL, GEMINI_API_BASE_URL). Update the count or rephrase (e.g., "The following environment variables...") to avoid confusing readers.