Skip to content

api-proxy: BYOK session_id body injection breaks Azure OpenAI /responses with HTTP 400 #4339

@zarenner

Description

@zarenner

Summary

When the Copilot provider is configured in BYOK mode against a non-Copilot OpenAI-compatible endpoint (e.g. Azure OpenAI), the api-proxy unconditionally injects a top-level session_id field into every upstream request body. Strict OpenAI-compatible servers (including Azure OpenAI's /openai/v1/responses) reject unknown body parameters with HTTP 400, causing every agent turn to fail:

CAPIError: 400 Unknown parameter: 'session_id'.
{
  "error": {
    "message": "Unknown parameter: 'session_id'.",
    "type": "invalid_request_error",
    "param": "session_id",
    "code": "unknown_parameter"
  }
}

The Copilot CLI then exhausts its retry budget and the run fails.

Versions

  • gh-aw-firewall v0.25.63
  • gh-aw v0.78.1
  • copilot CLI v1.0.58
  • Provider: Azure OpenAI, COPILOT_PROVIDER_BASE_URL=https://<resource>.openai.azure.com/openai/v1

Root cause

In containers/api-proxy/providers/copilot.js, createCopilotAdapter does:

const providerSessionId = (env.AWF_PROVIDER_SESSION_ID || '').trim() || undefined;
if (providerSessionId) {
  // ...header injection elided...
  if (!Object.hasOwn(byokExtraBodyFields, 'session_id')) {
    byokExtraBodyFields.session_id = providerSessionId;
  }
}

…and then composes a bodyTransform that calls injectByokExtraBodyFields on every request body whenever the BYOK API key is present.

The AWF_PROVIDER_SESSION_ID env var has a fallback chain in src/services/api-proxy-service-config.ts → resolveProviderSessionId:

const value = getConfigEnvValue(config, 'AWF_PROVIDER_SESSION_ID')
  ?? process.env.AWF_PROVIDER_SESSION_ID
  ?? process.env.GH_AW_GITHUB_RUN_ID
  ?? process.env.GITHUB_RUN_ID;

Since GitHub Actions always sets GITHUB_RUN_ID, the fallback always resolves to a non-empty value, so BYOK session_id body injection is on by default for every gh-aw run with a BYOK provider — including providers that don't accept the session_id field.

The x-session-id header injection in the same block is harmless against strict servers (unknown headers are ignored), but the body-field injection is not.

Reproduction

  1. Configure a gh-aw workflow with engine: copilot and BYOK against Azure OpenAI:
    engine:
      id: copilot
      env:
        COPILOT_PROVIDER_BASE_URL: "https://<resource>.openai.azure.com/openai/v1"
        COPILOT_MODEL: <deployment-name>
        COPILOT_PROVIDER_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
  2. Trigger the workflow.
  3. Observe in sandbox/agent/logs/process-*.log:
    CAPIError: 400 Unknown parameter: 'session_id'.
    
  4. Observe in sandbox/firewall/logs/api-proxy-logs/otel.jsonl that every POST /responses returns http.response.status_code: 400.

Impact

Every BYOK Copilot run against an OpenAI-compatible endpoint that enforces strict parameter validation fails immediately. This affects Azure OpenAI (always strict) and likely other gateways that pass through to Azure or otherwise validate request schemas.

Current workaround

In the workflow markdown, force AWF_PROVIDER_SESSION_ID to empty so resolveProviderSessionId returns undefined and the auto-injection is disabled:

engine:
  id: copilot
  env:
    AWF_PROVIDER_SESSION_ID: ${{ '' }}

(${{ '' }} is required because gh-aw's YAML loader drops bare "" values.)

Note: setting apiProxy.targets.copilot.extraBodyFields.session_id = "" from awf-config does not work — the guard is if (!Object.hasOwn(byokExtraBodyFields, 'session_id')), so providing any value (even empty string) still triggers injection of that value. Only an unset/empty AWF_PROVIDER_SESSION_ID actually disables it.

Suggested fixes (one or more)

  1. Don't enable BYOK body-field injection by default. Only inject session_id (and any other auto-fields) when the user has opted in — e.g. only when AWF_PROVIDER_SESSION_ID is explicitly set by the caller via additionalEnv / config file, not when it's only resolved from the GITHUB_RUN_ID fallback.
  2. Scope auto-injection to known-compatible targets. session_id is a Copilot-API convention; don't auto-inject it when COPILOT_PROVIDER_BASE_URL / COPILOT_API_TARGET points away from *.githubcopilot.com / *.ghe.com. The isGithubCopilotCatalogTarget helper already exists for the model-fallback policy and could be reused here.
  3. Document a first-class opt-out in awf-config.json (e.g. apiProxy.targets.copilot.disableSessionIdInjection: true) and a corresponding env var, so users don't have to know about the AWF_PROVIDER_SESSION_ID="" trick.

Option (2) seems most aligned with the existing model-fallback suppression for non-Copilot BYOK targets and would fix this without requiring user action.

Related files

  • containers/api-proxy/providers/copilot.jscreateCopilotAdapter, injectByokExtraBodyFields, parseByokExtraBodyFields
  • src/services/api-proxy-service-config.tsresolveProviderSessionId, buildProviderTargetEnv
  • containers/api-proxy/README.mdAWF_BYOK_EXTRA_BODY_FIELDS, AWF_PROVIDER_SESSION_ID docs

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions