From 1829607c49044ce6da809f27307c26d22b53d9dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:21:34 +0000 Subject: [PATCH 1/3] Initial plan From f68f9e9f0b04e8596fdabe28c6ca162775a64c13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:38:20 +0000 Subject: [PATCH 2/3] Fix Crush harness sending chat-completions to unresolvable api-proxy host Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../workflows/daily-arxiv-researcher.lock.yml | 24 +++++++++++------- .github/workflows/daily-code-metrics.lock.yml | 24 +++++++++++------- .github/workflows/shared/crush.md | 11 +++++--- .github/workflows/smoke-crush.lock.yml | 24 +++++++++++------- actions/setup/js/awf_reflect.cjs | 22 ++++++++++++++++ actions/setup/js/awf_reflect.test.cjs | 25 +++++++++++++++++++ 6 files changed, 99 insertions(+), 31 deletions(-) diff --git a/.github/workflows/daily-arxiv-researcher.lock.yml b/.github/workflows/daily-arxiv-researcher.lock.yml index 8c4cfef11ba..018da6104a4 100644 --- a/.github/workflows/daily-arxiv-researcher.lock.yml +++ b/.github/workflows/daily-arxiv-researcher.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"1c722f9789b54d464ae08c0534521df56dc25139772b30e1e1ba0d2c8726a725","body_hash":"8a0ae0a7a36920221c9cfc8d390ce6ca8ee22847f2bafa90bd7d62995bfb0e9d","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"611fdf11fb3eeba01ad4ddb0b1e9ec4126c27235fa8e1b4be1f1699fa44b1dce","body_hash":"8a0ae0a7a36920221c9cfc8d390ce6ca8ee22847f2bafa90bd7d62995bfb0e9d","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["DOCKER_PAT","DOCKER_USERNAME","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -860,7 +860,7 @@ jobs: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -923,9 +923,12 @@ jobs: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { @@ -1686,7 +1689,7 @@ jobs: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -1749,9 +1752,12 @@ jobs: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index a212a50850b..a25e1a7a527 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b350935d6806a46a1adcd80a8ae5e4b8930e4732183e53c4d3090f428776ea92","body_hash":"999291c33885ecbdbc7a407fb6f399b412507fb4ab69814ac9586f39db4fb4ca","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f239465ec5f7f1384cfb53683aef031723bb3648cd063d2c34aa659b0e511057","body_hash":"999291c33885ecbdbc7a407fb6f399b412507fb4ab69814ac9586f39db4fb4ca","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["DOCKER_PAT","DOCKER_USERNAME","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -970,7 +970,7 @@ jobs: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -1033,9 +1033,12 @@ jobs: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { @@ -2025,7 +2028,7 @@ jobs: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -2088,9 +2091,12 @@ jobs: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { diff --git a/.github/workflows/shared/crush.md b/.github/workflows/shared/crush.md index 4c1f1b7c14f..82b94fc9347 100644 --- a/.github/workflows/shared/crush.md +++ b/.github/workflows/shared/crush.md @@ -134,7 +134,7 @@ engine: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -197,9 +197,12 @@ engine: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { diff --git a/.github/workflows/smoke-crush.lock.yml b/.github/workflows/smoke-crush.lock.yml index 35331f82b3a..a30be5c3e72 100644 --- a/.github/workflows/smoke-crush.lock.yml +++ b/.github/workflows/smoke-crush.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"49739299ecb2786bbedbfc35f292c3d2787464274272a5d68637d73fb7393a0b","body_hash":"e5ee7908c0ded99864054d63fdfda9d352a7f6bbd6ae09ed51ee0f31773ecbf1","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"426260f273a02cc9f76af9bc0f13f540c05d8946aed6ed26e41496db71892071","body_hash":"e5ee7908c0ded99864054d63fdfda9d352a7f6bbd6ae09ed51ee0f31773ecbf1","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"has_pull_request":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -984,7 +984,7 @@ jobs: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -1047,9 +1047,12 @@ jobs: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { @@ -1906,7 +1909,7 @@ jobs: const { spawnSync } = require("child_process"); const { accessSync, constants, readFileSync, writeFileSync } = require("fs"); const { isAbsolute, join } = require("path"); - const { fetchAWFReflect, resolveProviderEndpointFromReflect } = require("./awf_reflect.cjs"); + const { fetchAWFReflect, resolveProviderEndpointFromReflect, deriveBaseUrlFromModelsURL } = require("./awf_reflect.cjs"); const [command, ...commandArgs] = process.argv.slice(2); const log = message => process.stderr.write(`[crush-harness] ${message}\n`); @@ -1969,9 +1972,12 @@ jobs: entry => entry?.configured === true && entry.provider === endpoint.endpointProvider ); if (typeof reflectedEndpoint?.models_url === "string") { - const modelsURL = new URL(reflectedEndpoint.models_url); - const basePath = modelsURL.pathname.replace(/\/models\/?$/i, ""); - baseUrl = `${modelsURL.origin}${basePath}`; + // Re-derive the base URL from models_url (rather than reusing endpoint.baseUrl, + // which points at the models-listing endpoint) while still applying the same + // api-proxy -> host.docker.internal HOSTALIASES bridge rewrite, so the crush + // binary's own chat-completions request never targets the unresolvable + // "api-proxy" hostname. + baseUrl = deriveBaseUrlFromModelsURL(reflectedEndpoint.models_url); } } if (!baseUrl) { diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index 9cfa21fcdc6..2f9826484a9 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -588,6 +588,27 @@ function endpointBaseUrl(endpoint) { return ""; } +/** + * Derive a base URL (origin + path prefix, with any trailing `/models` segment + * stripped) from a `models_url` value, applying the same api-proxy -> + * host.docker.internal HOSTALIASES bridge rewrite as `endpointBaseUrl`. + * + * Harnesses that need a base URL for chat-completions requests (rather than + * the models-listing endpoint) should use this instead of deriving the + * base URL from `models_url` inline, so the api-proxy hostname rewrite is + * never accidentally skipped. + * + * @param {string} modelsUrl + * @param {NodeJS.ProcessEnv} [env] + * @param {(path: string, encoding: BufferEncoding) => string} [readFileSync] + * @returns {string} + */ +function deriveBaseUrlFromModelsURL(modelsUrl, env = process.env, readFileSync = fs.readFileSync) { + const parsed = new URL(modelsUrl); + const basePath = parsed.pathname.replace(/\/models\/?$/i, ""); + return rewriteAPIProxyURLForHostBridge(`${parsed.origin}${basePath}`, env, readFileSync); +} + /** * Resolve a configured provider endpoint from AWF /reflect data. * @@ -846,6 +867,7 @@ if (typeof module !== "undefined" && module.exports) { hasAPIProxyLocalhostAlias, inferProviderTypeForModel, inferWireApiForModel, + deriveBaseUrlFromModelsURL, normalizeReflectProviderName, resolveOpenAICompatibleEndpointFromReflect, resolveProviderEndpointFromReflect, diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index aa9315e02be..cf27c9a903b 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -15,6 +15,7 @@ const { AWF_MODELS_URL_RETRY_MAX_MS, DEFAULT_API_PROXY_HOST_BRIDGE, GEMINI_MODEL_NAME_PREFIX, + deriveBaseUrlFromModelsURL, enrichReflectModels, extractModelIds, fetchAWFReflect, @@ -69,6 +70,30 @@ describe("awf_reflect.cjs", () => { }); }); + describe("deriveBaseUrlFromModelsURL", () => { + it("strips a trailing /models segment and leaves non-bridged hosts untouched", () => { + const env = {}; + const readFileSync = () => ""; + + expect(deriveBaseUrlFromModelsURL("http://api-proxy:10002/v1/models", env, readFileSync)).toBe("http://api-proxy:10002/v1"); + }); + + it("rewrites the api-proxy host to the HOSTALIASES bridge host, matching resolveProviderEndpointFromReflect", () => { + // Regression test: the crush harness previously derived its chat-completions + // base URL from models_url without reapplying the api-proxy -> host bridge + // rewrite, so it sent requests to the unresolvable "api-proxy" hostname even + // though resolveProviderEndpointFromReflect's baseUrl was already rewritten. + const env = { HOSTALIASES: "/tmp/aliases" }; + const readFileSync = () => "api-proxy localhost\n"; + + expect(deriveBaseUrlFromModelsURL("http://api-proxy:10002/v1/models", env, readFileSync)).toBe("http://host.docker.internal:10002/v1"); + }); + + it("defaults to process.env and fs.readFileSync when not provided, with no path prefix before /models", () => { + expect(deriveBaseUrlFromModelsURL("http://api-proxy:10002/models")).toBe("http://api-proxy:10002"); + }); + }); + describe("extractModelIds", () => { it("returns null for null input", () => { expect(extractModelIds(null)).toBeNull(); From 59876dd5ddea78a972683ddf489b1b3073962040 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:59:39 +0000 Subject: [PATCH 3/3] Apply host bridge rewrite to OpenAI-compatible endpoint resolution Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/awf_reflect.cjs | 7 +++++-- actions/setup/js/awf_reflect.test.cjs | 14 +++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index 2f9826484a9..75d3857371c 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -674,6 +674,8 @@ function resolveProviderEndpointFromReflect(options) { * provider?: string, * reflectData: ReflectData | null | undefined, * logger?: (msg: string) => void, + * env?: NodeJS.ProcessEnv, + * readFileSync?: (path: string, encoding: BufferEncoding) => string, * }} options * @returns {{ provider: string, endpointProvider: string, host: string, basePath: string } | null} */ @@ -712,8 +714,9 @@ function resolveOpenAICompatibleEndpointFromReflect(options) { path = path.replace(/\/models$/i, "/chat/completions"); const basePath = path.replace(/^\/+/, ""); const endpointProvider = String(endpoint.provider); - logger(`awf-reflect: provider=${provider} mapped to endpoint provider=${endpointProvider} host=${parsed.origin} basePath=${basePath}`); - return { provider, endpointProvider, host: parsed.origin, basePath }; + const host = rewriteAPIProxyURLForHostBridge(parsed.origin, options?.env, options?.readFileSync); + logger(`awf-reflect: provider=${provider} mapped to endpoint provider=${endpointProvider} host=${host} basePath=${basePath}`); + return { provider, endpointProvider, host, basePath }; } catch { logger(`awf-reflect: invalid endpoint URL for provider=${provider}`); return null; diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index cf27c9a903b..b76fc43697e 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -90,7 +90,7 @@ describe("awf_reflect.cjs", () => { }); it("defaults to process.env and fs.readFileSync when not provided, with no path prefix before /models", () => { - expect(deriveBaseUrlFromModelsURL("http://api-proxy:10002/models")).toBe("http://api-proxy:10002"); + expect(deriveBaseUrlFromModelsURL("http://example.test:10002/models")).toBe("http://example.test:10002"); }); }); @@ -221,6 +221,18 @@ describe("awf_reflect.cjs", () => { it("does not fall back to a different configured provider", () => { expect(resolveOpenAICompatibleEndpointFromReflect({ provider: "anthropic", reflectData, logger: () => {} })).toBeNull(); }); + + it("rewrites the host bridge for definition-based engine OpenAI-compatible endpoints", () => { + const env = { HOSTALIASES: "/tmp/aliases" }; + const readFileSync = () => "api-proxy localhost\n"; + + expect(resolveOpenAICompatibleEndpointFromReflect({ provider: "github", reflectData, logger: () => {}, env, readFileSync })).toEqual({ + provider: "github", + endpointProvider: "copilot", + host: "http://host.docker.internal:10002", + basePath: "chat/completions", + }); + }); }); it("does nothing when all configured endpoints already have models", async () => {