Skip to content

feat: support custom OpenAI and Anthropic API targets in AWF sandbox#20631

Merged
lpcox merged 2 commits intomainfrom
claude/fix-api-proxy-custom-urls
Mar 12, 2026
Merged

feat: support custom OpenAI and Anthropic API targets in AWF sandbox#20631
lpcox merged 2 commits intomainfrom
claude/fix-api-proxy-custom-urls

Conversation

@Claude
Copy link
Contributor

@Claude Claude AI commented Mar 12, 2026

The AWF API proxy hardcoded api.openai.com and api.anthropic.com as upstream targets, preventing use with internal LLM routers, Azure OpenAI, or self-hosted OpenAI-compatible endpoints. This blocked workflows from leveraging AWF's credential isolation and firewall features with custom API endpoints.

Changes

Automatic API target detection and forwarding:

  • Added extractAPITargetHost() to parse hostnames from OPENAI_BASE_URL and ANTHROPIC_BASE_URL in engine.env
  • Modified BuildAWFArgs() to automatically append --openai-api-target and --anthropic-api-target flags to AWF when custom URLs are detected
  • Complements gh-aw-firewall#1249 which added the corresponding flag support in AWF

Example usage:

engine:
  id: codex
  model: gpt-4
  env:
    OPENAI_BASE_URL: "https://llm-router.internal.example.com/v1"
    OPENAI_API_KEY: ${{ secrets.LLM_ROUTER_KEY }}

network:
  allowed:
    - github.com
    - llm-router.internal.example.com

Compiles to:

awf --enable-api-proxy \
    --openai-api-target llm-router.internal.example.com \
    --allow-domains "github.com,llm-router.internal.example.com" \
    -- <agent-command>

The API proxy now forwards OpenAI requests to llm-router.internal.example.com instead of api.openai.com, while preserving credential isolation and firewall enforcement.

Testing

  • Unit tests cover URL parsing (HTTPS/HTTP, with/without ports, Azure OpenAI formats)
  • Integration tests verify flag generation for Codex and Claude engines
  • End-to-end compilation tests confirm proper flag inclusion in .lock.yml files

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw HEAD 86_64/git git rev-�� --show-toplevel git /usr/bin/git --abbrev-ref HEAD 0/x64/bin/git git (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha m /tmp/go-build1633339647/b005/vet.cfg 3339647/b336/vet.cfg [:lower:] git it /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� -unreachable=false /tmp/go-build1633339647/b211/vet.cfg /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha --abbrev-ref HEAD ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet --abbrev-ref HEAD (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha flib/difflib.go HEAD x_amd64/compile --abbrev-ref HEAD 0/x64/bin/git x_amd64/compile rev-�� --abbrev-ref HEAD x_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha -bool -buildtags /usr/bin/tail -errorsas -ifaceassert -nilfunc tail -n 1 -tests /usr/bin/base64 (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel x_amd64/vet /usr/bin/git --abbrev-ref HEAD x_amd64/vet git rev-�� --show-toplevel x_amd64/vet /usr/bin/git --abbrev-ref HEAD x_amd64/vet git (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha developer-action--abbrev-ref ons/[^/]*)?/[^/]HEAD (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha 0/x64/bin/git ons/[^/]*)?/[^/]HEAD /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet --abbrev-ref HEAD /usr/bin/dirname--show-toplevel /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� -unreachable=false /tmp/go-build1633339647/b093/vet.cfg 3339647/b318/vet.cfg d -n 10 head tnet/tools/git /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel git /usr/bin/git celain --ignore-git HEAD x_amd64/vet git rev-�� --show-toplevel x_amd64/vet /usr/bin/git --abbrev-ref HEAD x_amd64/vet git (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha /usr/local/bin/g--abbrev-ref git /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet --abbrev-ref HEAD /opt/hostedtoolc--show-toplevel /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� -unreachable=false ner/.claude/shell-snapshots/snapshot-bash-1773287433690-kf9144.sh &amp;&amp; { shopt -u extglob || setoprev-parse 3339647/b291/vet.cfg --abbrev-ref -linux/rg nfig/composer/ve--show-toplevel /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha /home/REDACTED/.lo--abbrev-ref git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha n-dir/git git (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha /opt/hostedtoolc--abbrev-ref git (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha --abbrev-ref HEAD 64/pkg/tool/linux_amd64/vet --abbrev-ref HEAD n-dir/git 64/pkg/tool/linux_amd64/vet rev-�� --abbrev-ref HEAD 64/bin/git --abbrev-ref HEAD t git (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha --abbrev-ref HEAD git --abbrev-ref HEAD cal/bin/git git rev-�� --abbrev-ref HEAD x_amd64/vet (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha --abbrev-ref HEAD /home/REDACTED/work/_temp/uv-python-dir/git --abbrev-ref HEAD 0/x64/bin/git git rev-�� --abbrev-ref HEAD x_amd64/vet (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Add --openai-api-target and --anthropic-api-target flags to AWF when custom
OPENAI_BASE_URL or ANTHROPIC_BASE_URL are configured in engine.env. This enables
AWF's credential isolation and firewall features to work with internal LLM routers,
Azure OpenAI endpoints, and other custom OpenAI-compatible APIs.

Implementation:
- Added extractAPITargetHost() to parse hostnames from custom API base URLs
- Modified BuildAWFArgs() to automatically add API target flags when custom URLs are detected
- Created comprehensive tests covering URL parsing, flag generation, and engine execution

Fixes issue where API proxy hardcoded api.openai.com/api.anthropic.com, preventing
use with internal LLM routers and custom endpoints.

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
@Claude Claude AI changed the title [WIP] Fix API proxy to forward custom OPENAI_BASE_URL and ANTHROPIC_BASE_URL feat: support custom OpenAI and Anthropic API targets in AWF sandbox Mar 12, 2026
@github-actions
Copy link
Contributor

Hey @Claude and @lpcox 👋 — great work on this feature! Adding support for custom OpenAI and Anthropic API targets directly into BuildAWFArgs() is a clean, well-scoped change that meaningfully unblocks workflows using internal LLM routers and Azure OpenAI while preserving AWF's credential isolation and firewall enforcement.

The PR is well-structured: a focused implementation in awf_helpers.go backed by thorough tests in the new awf_helpers_test.go (8 unit cases in TestExtractAPITargetHost, 4 integration-style cases in TestAWFCustomAPITargetFlags, and engine-level tests in TestEngineExecutionWithCustomAPITarget). The description is detailed and includes a clear usage example. ✅

Two small things worth a look before marking ready for review:

  • Draft status & companion dependency — the PR references gh-aw-firewall#1249 for the corresponding --openai-api-target / --anthropic-api-target flag support in AWF. Once that lands, it's worth confirming the flag names here match exactly and then promoting this out of draft.
  • URL parsing robustnessextractAPITargetHost uses manual strings.Index slicing instead of Go's net/url package. The current logic handles all tested formats fine, but url.Parse would be more idiomatic and would correctly handle edge cases like query strings or unusual schemes without extra code.

Overall this is a solid, well-tested contribution — nice job! 🟢

Generated by Contribution Check ·

@github-actions github-actions bot added the lgtm label Mar 12, 2026
@lpcox lpcox marked this pull request as ready for review March 12, 2026 14:02
Copilot AI review requested due to automatic review settings March 12, 2026 14:02
@lpcox lpcox merged commit d959c0b into main Mar 12, 2026
80 checks passed
@lpcox lpcox deleted the claude/fix-api-proxy-custom-urls branch March 12, 2026 14:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for forwarding AWF’s API proxy to custom OpenAI/Anthropic upstream hosts (e.g., internal routers, Azure OpenAI) by deriving target hosts from engine env configuration and passing them as AWF flags.

Changes:

  • Added extractAPITargetHost() helper to derive the upstream hostname from OPENAI_BASE_URL / ANTHROPIC_BASE_URL.
  • Updated BuildAWFArgs() to append --openai-api-target / --anthropic-api-target when custom base URLs are configured.
  • Added unit tests for host extraction and flag generation in both direct AWF args and engine execution steps.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
pkg/workflow/awf_helpers.go Adds API target host extraction and appends new AWF --*-api-target flags based on engine env.
pkg/workflow/awf_helpers_test.go Adds unit tests validating URL/host parsing and verifying flag inclusion in generated AWF args and execution steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +302 to +317
// Extract hostname from URL
// URLs can be:
// - "https://llm-router.internal.example.com/v1" → "llm-router.internal.example.com"
// - "http://localhost:8080/v1" → "localhost:8080"
// - "api.openai.com" → "api.openai.com" (treated as hostname)

// Remove protocol prefix if present
host := baseURL
if idx := strings.Index(host, "://"); idx != -1 {
host = host[idx+3:]
}

// Remove path suffix if present (everything after first /)
if idx := strings.Index(host, "/"); idx != -1 {
host = host[:idx]
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractAPITargetHost does manual string slicing and fails to strip URL query/fragment/userinfo. For example https://example.com?x=1 would return example.com?x=1, and https://user:pass@host/v1 would return user:pass@host, which is not a valid --*-api-target host and can break AWF routing. Consider parsing with net/url (optionally prefixing a scheme when missing), then returning u.Host (or u.Hostname() + optional u.Port()), and trimming whitespace.

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +321
// Validate that we have a non-empty hostname
if host == "" {
awfHelpersLog.Printf("Invalid %s URL (no hostname): %s", envVar, baseURL)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log line prints the full baseURL value. If a user provides a URL containing embedded credentials (userinfo) or sensitive query params, it could leak into workflow logs. Safer to log only the env var name and a sanitized/parsed host (or omit the URL entirely) on error.

Suggested change
// Validate that we have a non-empty hostname
if host == "" {
awfHelpersLog.Printf("Invalid %s URL (no hostname): %s", envVar, baseURL)
// Remove userinfo (credentials) if present (e.g., "user:pass@host")
// Keep only the part after '@' so that credentials are never logged.
if idx := strings.LastIndex(host, "@"); idx != -1 {
host = host[idx+1:]
}
// Remove query or fragment if present (e.g., "host:8080?api_key=..." or "host#section")
if idx := strings.IndexAny(host, "?#"); idx != -1 {
host = host[:idx]
}
// Validate that we have a non-empty hostname
if host == "" {
awfHelpersLog.Printf("Invalid %s URL (no hostname)", envVar)

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +119
tests := []struct {
name string
workflowData *WorkflowData
envVar string
expected string
}{
{
name: "extracts hostname from HTTPS URL with path",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"OPENAI_BASE_URL": "https://llm-router.internal.example.com/v1",
},
},
},
envVar: "OPENAI_BASE_URL",
expected: "llm-router.internal.example.com",
},
{
name: "extracts hostname from HTTP URL with port and path",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"ANTHROPIC_BASE_URL": "http://localhost:8080/v1",
},
},
},
envVar: "ANTHROPIC_BASE_URL",
expected: "localhost:8080",
},
{
name: "handles hostname without protocol or path",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"OPENAI_BASE_URL": "api.openai.com",
},
},
},
envVar: "OPENAI_BASE_URL",
expected: "api.openai.com",
},
{
name: "handles hostname with port but no protocol",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"OPENAI_BASE_URL": "localhost:8000",
},
},
},
envVar: "OPENAI_BASE_URL",
expected: "localhost:8000",
},
{
name: "returns empty string when env var not set",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"OTHER_VAR": "value",
},
},
},
envVar: "OPENAI_BASE_URL",
expected: "",
},
{
name: "returns empty string when engine config is nil",
workflowData: &WorkflowData{
EngineConfig: nil,
},
envVar: "OPENAI_BASE_URL",
expected: "",
},
{
name: "returns empty string when workflow data is nil",
workflowData: nil,
envVar: "OPENAI_BASE_URL",
expected: "",
},
{
name: "returns empty string for empty URL",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"OPENAI_BASE_URL": "",
},
},
},
envVar: "OPENAI_BASE_URL",
expected: "",
},
{
name: "extracts Azure OpenAI endpoint hostname",
workflowData: &WorkflowData{
EngineConfig: &EngineConfig{
Env: map[string]string{
"OPENAI_BASE_URL": "https://my-resource.openai.azure.com/openai/deployments/gpt-4",
},
},
},
envVar: "OPENAI_BASE_URL",
expected: "my-resource.openai.azure.com",
},
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests claim URL parsing is covered, but there are no cases for URLs with query/fragment or userinfo (e.g. https://example.com?api-version=..., https://user:pass@host/v1). Adding these would prevent regressions once extractAPITargetHost is updated to use proper URL parsing.

Copilot uses AI. Check for mistakes.
github-actions bot added a commit that referenced this pull request Mar 12, 2026
…s.md

Add a new 'Custom API Endpoints' subsection under 'Engine Environment
Variables' documenting that OPENAI_BASE_URL (codex) and ANTHROPIC_BASE_URL
(claude) are automatically picked up by the AWF sandbox proxy to route
API calls to internal LLM routers, Azure OpenAI, or other custom endpoints.

From PR #20631 (merged 2026-03-12T14:02Z, after DDUw's 06:23Z run).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API proxy does not forward to custom OPENAI_BASE_URL / ANTHROPIC_BASE_URL endpoints (e.g., internal LLM routers)

3 participants