From c224e5eab7fff0f1044a3a38993edee970b72021 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:13:07 +0000 Subject: [PATCH 1/3] Initial plan From 68a6001e2334e75199ef9a5e58c6c7f61c88dec0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:17:13 +0000 Subject: [PATCH 2/3] chore: initial plan for extractAPIBasePath implementation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/04cc1879-ebb0-4ab8-93a5-0dccdddb4f85 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../daily-multi-device-docs-tester.lock.yml | 42 ++++++++-------- .github/workflows/unbloat-docs.lock.yml | 50 +++++++++---------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 18a5d196ac..0e6f795d99 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -27,7 +27,7 @@ # - shared/docs-server-lifecycle.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f5a4802459021b8361db58fb67d33ae472aaffe53fceda01e56ad10d735f2c27","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6747fad76bb2d4f0fe944fc4d92885fc495ec8c1ce60b1e268fd169ac9268de7","strict":true,"agent_id":"claude"} name: "Multi-Device Docs Tester" "on": @@ -143,15 +143,15 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_55a476ca092f3fd9_EOF' + cat << 'GH_AW_PROMPT_c81a109c621158fb_EOF' - GH_AW_PROMPT_55a476ca092f3fd9_EOF + GH_AW_PROMPT_c81a109c621158fb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_55a476ca092f3fd9_EOF' + cat << 'GH_AW_PROMPT_c81a109c621158fb_EOF' Tools: create_issue, upload_asset, missing_tool, missing_data, noop @@ -185,20 +185,20 @@ jobs: {{/if}} - GH_AW_PROMPT_55a476ca092f3fd9_EOF + GH_AW_PROMPT_c81a109c621158fb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_55a476ca092f3fd9_EOF' + cat << 'GH_AW_PROMPT_c81a109c621158fb_EOF' - GH_AW_PROMPT_55a476ca092f3fd9_EOF - cat << 'GH_AW_PROMPT_55a476ca092f3fd9_EOF' + GH_AW_PROMPT_c81a109c621158fb_EOF + cat << 'GH_AW_PROMPT_c81a109c621158fb_EOF' {{#runtime-import .github/workflows/shared/docs-server-lifecycle.md}} - GH_AW_PROMPT_55a476ca092f3fd9_EOF - cat << 'GH_AW_PROMPT_55a476ca092f3fd9_EOF' + GH_AW_PROMPT_c81a109c621158fb_EOF + cat << 'GH_AW_PROMPT_c81a109c621158fb_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} - GH_AW_PROMPT_55a476ca092f3fd9_EOF - cat << 'GH_AW_PROMPT_55a476ca092f3fd9_EOF' + GH_AW_PROMPT_c81a109c621158fb_EOF + cat << 'GH_AW_PROMPT_c81a109c621158fb_EOF' {{#runtime-import .github/workflows/daily-multi-device-docs-tester.md}} - GH_AW_PROMPT_55a476ca092f3fd9_EOF + GH_AW_PROMPT_c81a109c621158fb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -372,12 +372,12 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_8a777334cac793e2_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_c93bece35bcc7ae1_EOF' {"create_issue":{"expires":48,"labels":["cookie"],"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"upload_asset":{"allowed-exts":[".png",".jpg",".jpeg"],"branch":"assets/${{ github.workflow }}","max-size":10240}} - GH_AW_SAFE_OUTPUTS_CONFIG_8a777334cac793e2_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_c93bece35bcc7ae1_EOF - name: Write Safe Outputs Tools run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_a4806cbf9d506369_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_d2eba1dc753a725e_EOF' { "description_suffixes": { "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"cookie\"] will be automatically added.", @@ -386,8 +386,8 @@ jobs: "repo_params": {}, "dynamic_tools": [] } - GH_AW_SAFE_OUTPUTS_TOOLS_META_a4806cbf9d506369_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_310b49412733c666_EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_META_d2eba1dc753a725e_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_03255beea463aa05_EOF' { "create_issue": { "defaultMax": 1, @@ -489,7 +489,7 @@ jobs: } } } - GH_AW_SAFE_OUTPUTS_VALIDATION_310b49412733c666_EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_03255beea463aa05_EOF node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config @@ -560,7 +560,7 @@ jobs: export GH_AW_ENGINE="claude" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6' - cat << GH_AW_MCP_CONFIG_47aa03a5c9473b5a_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_6f62c6e537c76192_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { @@ -624,7 +624,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_47aa03a5c9473b5a_EOF + GH_AW_MCP_CONFIG_6f62c6e537c76192_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 39009e85ae..4a01f4fa7b 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -28,7 +28,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"618b829bbc7f47e9574ed3e1557438fae0bfc6ca453edab14981b726236131ba","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1d5cfd4f91bd8fcf04b0386757779cc674a35eb53bac2a45273366b85d389beb","strict":true,"agent_id":"claude"} name: "Documentation Unbloat" "on": @@ -187,9 +187,9 @@ jobs: run: | bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh { - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' - GH_AW_PROMPT_3d06355205885e9a_EOF + GH_AW_PROMPT_2d537e0f4ae05d79_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" @@ -197,12 +197,12 @@ jobs: cat "${RUNNER_TEMP}/gh-aw/prompts/qmd_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' Tools: add_comment, create_pull_request, upload_asset, missing_tool, missing_data, noop - GH_AW_PROMPT_3d06355205885e9a_EOF + GH_AW_PROMPT_2d537e0f4ae05d79_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' upload_asset: provide a file path; returns a URL; assets are published after the workflow completes (safeoutputs). @@ -234,23 +234,23 @@ jobs: {{/if}} - GH_AW_PROMPT_3d06355205885e9a_EOF + GH_AW_PROMPT_2d537e0f4ae05d79_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' - GH_AW_PROMPT_3d06355205885e9a_EOF - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + GH_AW_PROMPT_2d537e0f4ae05d79_EOF + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} - GH_AW_PROMPT_3d06355205885e9a_EOF - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + GH_AW_PROMPT_2d537e0f4ae05d79_EOF + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' {{#runtime-import .github/workflows/shared/docs-server-lifecycle.md}} - GH_AW_PROMPT_3d06355205885e9a_EOF - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + GH_AW_PROMPT_2d537e0f4ae05d79_EOF + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' {{#runtime-import .github/workflows/shared/mcp/qmd-docs.md}} - GH_AW_PROMPT_3d06355205885e9a_EOF - cat << 'GH_AW_PROMPT_3d06355205885e9a_EOF' + GH_AW_PROMPT_2d537e0f4ae05d79_EOF + cat << 'GH_AW_PROMPT_2d537e0f4ae05d79_EOF' {{#runtime-import .github/workflows/unbloat-docs.md}} - GH_AW_PROMPT_3d06355205885e9a_EOF + GH_AW_PROMPT_2d537e0f4ae05d79_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -466,12 +466,12 @@ jobs: mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_8c6de75ca9d63c58_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_815bd590907de7b8_EOF' {"add_comment":{"max":1},"create_pull_request":{"auto_merge":true,"draft":true,"expires":48,"fallback_as_issue":false,"labels":["documentation","automation"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"reviewers":["copilot"],"title_prefix":"[docs] "},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"upload_asset":{"allowed-exts":[".png",".jpg",".jpeg"],"branch":"assets/${{ github.workflow }}","max-size":10240}} - GH_AW_SAFE_OUTPUTS_CONFIG_8c6de75ca9d63c58_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_815bd590907de7b8_EOF - name: Write Safe Outputs Tools run: | - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_2bd18671c86f4c47_EOF' + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_6882e4174d89c62c_EOF' { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added.", @@ -481,8 +481,8 @@ jobs: "repo_params": {}, "dynamic_tools": [] } - GH_AW_SAFE_OUTPUTS_TOOLS_META_2bd18671c86f4c47_EOF - cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_b9c0a140e277dd38_EOF' + GH_AW_SAFE_OUTPUTS_TOOLS_META_6882e4174d89c62c_EOF + cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_7e6df8d287392c6f_EOF' { "add_comment": { "defaultMax": 1, @@ -605,7 +605,7 @@ jobs: } } } - GH_AW_SAFE_OUTPUTS_VALIDATION_b9c0a140e277dd38_EOF + GH_AW_SAFE_OUTPUTS_VALIDATION_7e6df8d287392c6f_EOF node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config @@ -710,7 +710,7 @@ jobs: export GH_AW_ENGINE="claude" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.6' - cat << GH_AW_MCP_CONFIG_c8fb4c5e383447e3_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh + cat << GH_AW_MCP_CONFIG_249bb59b60f3bd35_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh { "mcpServers": { "github": { @@ -787,7 +787,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_c8fb4c5e383447e3_EOF + GH_AW_MCP_CONFIG_249bb59b60f3bd35_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: From d7923df3d4682eb77300756ae50e5f202aab6277 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:38:38 +0000 Subject: [PATCH 3/3] fix: pass --openai-api-base-path/--anthropic-api-base-path to AWF when URL contains path When OPENAI_BASE_URL or ANTHROPIC_BASE_URL in engine.env contains a path prefix (e.g., /serving-endpoints), the compiler was stripping the path and only passing the hostname via --openai-api-target. This caused API requests to go to the wrong URL, returning 404. This fix adds extractAPIBasePath() to extract the path component, and passes it via --openai-api-base-path and --anthropic-api-base-path to AWF. Fixes: github/gh-aw#21320 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/04cc1879-ebb0-4ab8-93a5-0dccdddb4f85 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/awf_helpers.go | 56 ++++++++++++ pkg/workflow/awf_helpers_test.go | 146 +++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index f97884d9b6..4fc68614be 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -221,6 +221,21 @@ func BuildAWFArgs(config AWFCommandConfig) []string { awfHelpersLog.Printf("Added --anthropic-api-target=%s", anthropicTarget) } + // Pass base path if URL contains a path component + // This is required for endpoints with path prefixes (e.g., Databricks /serving-endpoints, + // Azure OpenAI /openai/deployments/, corporate LLM routers with path-based routing) + openaiBasePath := extractAPIBasePath(config.WorkflowData, "OPENAI_BASE_URL") + if openaiBasePath != "" { + awfArgs = append(awfArgs, "--openai-api-base-path", openaiBasePath) + awfHelpersLog.Printf("Added --openai-api-base-path=%s", openaiBasePath) + } + + anthropicBasePath := extractAPIBasePath(config.WorkflowData, "ANTHROPIC_BASE_URL") + if anthropicBasePath != "" { + awfArgs = append(awfArgs, "--anthropic-api-base-path", anthropicBasePath) + awfHelpersLog.Printf("Added --anthropic-api-base-path=%s", anthropicBasePath) + } + // Add Copilot API target for custom Copilot endpoints (GHEC, GHES, or custom). // Resolved from engine.api-target (explicit) or GITHUB_COPILOT_BASE_URL in engine.env (implicit). if copilotTarget := GetCopilotAPITarget(config.WorkflowData); copilotTarget != "" { @@ -355,6 +370,47 @@ func extractAPITargetHost(workflowData *WorkflowData, envVar string) string { return host } +// extractAPIBasePath extracts the path component from a custom API base URL in engine.env. +// Returns the path prefix (e.g., "/serving-endpoints") or empty string if no path is present. +// Root-only paths ("/") and empty paths return empty string. +// +// This is used to pass --openai-api-base-path and --anthropic-api-base-path to AWF when +// the configured base URL contains a path (e.g., Databricks serving endpoints, Azure OpenAI +// deployments, or corporate LLM routers with path-based routing). +func extractAPIBasePath(workflowData *WorkflowData, envVar string) string { + if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.Env == nil { + return "" + } + + baseURL, exists := workflowData.EngineConfig.Env[envVar] + if !exists || baseURL == "" { + return "" + } + + // Remove protocol prefix if present + host := baseURL + if idx := strings.Index(host, "://"); idx != -1 { + host = host[idx+3:] + } + + // Extract path (everything after the first /) + if idx := strings.Index(host, "/"); idx != -1 { + path := host[idx:] // e.g., "/serving-endpoints" + // Strip query string or fragment if present + if qi := strings.IndexAny(path, "?#"); qi != -1 { + path = path[:qi] + } + // Remove trailing slashes; a root-only path "/" becomes "" and returns empty + path = strings.TrimRight(path, "/") + if path != "" { + awfHelpersLog.Printf("Extracted API base path from %s: %s", envVar, path) + return path + } + } + + return "" +} + // GetCopilotAPITarget returns the effective Copilot API target hostname, checking in order: // 1. engine.api-target (explicit, takes precedence) // 2. GITHUB_COPILOT_BASE_URL in engine.env (implicit, derived from the configured Copilot base URL) diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 9439d5b1af..9ba93f1ba2 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -249,6 +249,152 @@ func TestAWFCustomAPITargetFlags(t *testing.T) { }) } +// TestExtractAPIBasePath tests the extractAPIBasePath function that extracts +// path components from custom API base URLs in engine.env +func TestExtractAPIBasePath(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + {"databricks serving endpoint", "https://host.com/serving-endpoints", "/serving-endpoints"}, + {"azure openai deployment", "https://host.com/openai/deployments/gpt-4", "/openai/deployments/gpt-4"}, + {"simple path", "https://host.com/v1", "/v1"}, + {"trailing slash stripped", "https://host.com/api/", "/api"}, + {"multiple trailing slashes stripped", "https://host.com/api///", "/api"}, + {"no path", "https://host.com", ""}, + {"bare hostname", "host.com", ""}, + {"root path only", "https://host.com/", ""}, + {"query string stripped", "https://host.com/api?param=value", "/api"}, + {"fragment stripped", "https://host.com/api#section", "/api"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + Env: map[string]string{ + "OPENAI_BASE_URL": tt.url, + }, + }, + } + result := extractAPIBasePath(workflowData, "OPENAI_BASE_URL") + assert.Equal(t, tt.expected, result, "Extracted base path should match expected value") + }) + } + + t.Run("returns empty string when workflow data is nil", func(t *testing.T) { + result := extractAPIBasePath(nil, "OPENAI_BASE_URL") + assert.Empty(t, result, "Should return empty string for nil workflow data") + }) + + t.Run("returns empty string when engine config is nil", func(t *testing.T) { + workflowData := &WorkflowData{EngineConfig: nil} + result := extractAPIBasePath(workflowData, "OPENAI_BASE_URL") + assert.Empty(t, result, "Should return empty string when engine config is nil") + }) + + t.Run("returns empty string when env var not set", func(t *testing.T) { + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + Env: map[string]string{"OTHER_VAR": "value"}, + }, + } + result := extractAPIBasePath(workflowData, "OPENAI_BASE_URL") + assert.Empty(t, result, "Should return empty string when env var not set") + }) +} + +// TestAWFBasePathFlags tests that BuildAWFArgs includes --openai-api-base-path and +// --anthropic-api-base-path when the configured URLs contain a path component +func TestAWFBasePathFlags(t *testing.T) { + t.Run("includes openai-api-base-path when OPENAI_BASE_URL has path component", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "codex", + Env: map[string]string{ + "OPENAI_BASE_URL": "https://stone-dataplatform.cloud.databricks.com/serving-endpoints", + "OPENAI_API_KEY": "${{ secrets.DATABRICKS_KEY }}", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + + config := AWFCommandConfig{ + EngineName: "codex", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.Contains(t, argsStr, "--openai-api-target", "Should include --openai-api-target flag") + assert.Contains(t, argsStr, "--openai-api-base-path", "Should include --openai-api-base-path flag") + assert.Contains(t, argsStr, "/serving-endpoints", "Should include the path component") + }) + + t.Run("includes anthropic-api-base-path when ANTHROPIC_BASE_URL has path component", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "claude", + Env: map[string]string{ + "ANTHROPIC_BASE_URL": "https://proxy.company.com/anthropic/v1", + "ANTHROPIC_API_KEY": "${{ secrets.ANTHROPIC_KEY }}", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + + config := AWFCommandConfig{ + EngineName: "claude", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.Contains(t, argsStr, "--anthropic-api-target", "Should include --anthropic-api-target flag") + assert.Contains(t, argsStr, "--anthropic-api-base-path", "Should include --anthropic-api-base-path flag") + assert.Contains(t, argsStr, "/anthropic/v1", "Should include the path component") + }) + + t.Run("does not include base-path flags when URLs have no path", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "codex", + Env: map[string]string{ + "OPENAI_BASE_URL": "https://openai-proxy.company.com", + "ANTHROPIC_BASE_URL": "https://anthropic-proxy.company.com", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + + config := AWFCommandConfig{ + EngineName: "codex", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.NotContains(t, argsStr, "--openai-api-base-path", "Should not include --openai-api-base-path when no path in URL") + assert.NotContains(t, argsStr, "--anthropic-api-base-path", "Should not include --anthropic-api-base-path when no path in URL") + }) +} + // TestBuildAWFArgsAuditDir tests that BuildAWFArgs always includes --audit-dir // pointing to the AWF audit directory for policy-manifest.json and other audit files func TestBuildAWFArgsAuditDir(t *testing.T) {