Skip to content

feat: add Azure OpenAI apiFormat for Chat Completions and Responses - #2626

Open
jeremiedeveloper wants to merge 2 commits into
kagent-dev:mainfrom
jeremiedeveloper:feat/azure-openai-api-format
Open

feat: add Azure OpenAI apiFormat for Chat Completions and Responses#2626
jeremiedeveloper wants to merge 2 commits into
kagent-dev:mainfrom
jeremiedeveloper:feat/azure-openai-api-format

Conversation

@jeremiedeveloper

Copy link
Copy Markdown

Fixes #2517

Summary

Adds an apiFormat discriminator on ModelConfig so Azure OpenAI (and OpenAI-compatible) models can use either Chat Completions or the Responses API.

apiFormat Behavior Azure HTTP path
chatCompletions (default, omitted = this) Existing behavior POST {endpoint}/openai/deployments/{deployment}/chat/completions?api-version=...
responses Azure OpenAI v1 Responses POST {endpoint}/openai/v1/responses — deployment name is the JSON model; no api-version query

Needed because some Azure / Foundry deployments (newer GPT-5 class, etc.) reject Chat Completions. Existing configs stay valid: the field is optional and CEL/Helm default to Chat Completions.

User-facing config

apiVersion: kagent.dev/v1alpha3
kind: ModelConfig
spec:
  provider: AzureOpenAI
  model: gpt-5
  azureOpenAI:
    azureEndpoint: https://YOUR_RESOURCE.openai.azure.com
    azureDeployment: gpt-5
    apiVersion: "2024-06-01"
    apiFormat: responses   # or chatCompletions

Helm:

providers:
  azureOpenAI:
    config:
      apiFormat: responses

Example manifest: examples/modelconfig-azure-openai-responses.yaml (placeholder secret, same style as examples/modelconfig-with-tls.yaml).

What changed, by file

API / CRD

  • go/api/v1alpha3/modelconfig_types.goOpenAIAPIFormat enum (chatCompletions | responses) on both openAI and azureOpenAI. Kubebuilder default chatCompletions. Comments describe the two Azure URL shapes.
  • go/api/v1alpha3/modelconfig_cel_test.go — CEL accepts both values; rejects unknown apiFormat.
  • go/api/v1alpha3/zz_generated.deepcopy.go, go/api/config/crd/bases/kagent.dev_modelconfigs.yaml, helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml — generated CRD so the field is served on v1alpha3.
  • go/api/adk/types.goAPIFormat on the translated ADK Azure/OpenAI model so runtimes see it.
  • ui/src/types/index.ts — TypeScript union for the UI.

Go runtime

  • go/adk/pkg/internal/azureai/azureai.go — shared Azure OpenAI client. Responses: false (default) keeps {endpoint}/openai/deployments/{dep}/ + api-version. Responses: true uses {endpoint}/openai/v1/ and does not send api-version. Auth is still Api-Key (or Azure AD), with Authorization stripped so a leftover OPENAI_API_KEY is not leaked to Azure.
  • go/adk/pkg/models/openai.goNewAzureOpenAIModel* sets ClientConfig.Responses from config.APIFormat == responses and logs apiFormat.
  • go/adk/pkg/agent/agent.go — agent construction passes apiFormat through.

Translators (declarative + v2)

  • go/core/internal/controller/translator/agent/adk_api_translator.go — copies ModelConfig.spec.azureOpenAI.apiFormat / openAI.apiFormat onto adk.AzureOpenAI / adk.OpenAI.
  • go/core/v2/translator/model.go — same for Harness / AgentTemplate revisions (KAGENT_CONFIG_JSON).

Python runtime

  • python/packages/kagent-adk/src/kagent/adk/types.pyapi_format on the Azure/OpenAI model config; to_agent() passes it into AzureOpenAI.
  • python/packages/kagent-adk/src/kagent/adk/models/_openai.pyapi_format: chatCompletions | responses. On Azure + responses, builds AsyncOpenAI(base_url="{endpoint}/openai/v1/", Api-Key header) instead of AsyncAzureOpenAI. generate_content_async branches to the Responses path.
  • python/packages/kagent-adk/src/kagent/adk/models/_openai_responses.py — converts genai Content ↔ Responses input items (text, function calls/outputs, images) and streams responses.create.

Helm / docs

  • helm/kagent/values.yaml — commented apiFormat: responses under Azure.
  • helm/kagent/tests/modelconfig_test.yaml — helm-unittest: when set, the rendered ModelConfig has spec.azureOpenAI.apiFormat: responses.
  • helm/README.md, docs/architecture/crds-and-types.md — document the field.
  • .gitignore — comment that .local/ is Kind/Azure smoke only (keys, values.local.yaml, reports). Does not ignore any feature source.

Tests

Unit (CI, no Azure account)

Go — HTTP path capture with httptest

  • go/adk/pkg/internal/azureai/azureai_test.go
  • go/adk/pkg/models/azure_openai_test.go

Assert:

  • default / chatCompletions/openai/deployments/{deployment}/chat/completions + api-version=2024-06-01
  • responses/openai/v1/responses and empty api-version
  • Responses GenerateContent still returns model text

Go — translator

  • go/core/internal/controller/translator/agent/azure_openai_translator_test.goapiFormat: responses lands on adk.AzureOpenAI.APIFormat; omitted → empty (runtime default).

Python — client + generate_content

  • python/packages/kagent-adk/tests/unittests/models/test_openai.py

Assert:

  • Azure api_format=responses constructs AsyncOpenAI with base_url=.../openai/v1/ and Api-Key; no api_version
  • default Azure still uses AsyncAzureOpenAI(azure_endpoint, api_version)
  • generate_content_async with responses calls client.responses.create, not chat.completions.create
  • api_format=chatCompletions calls chat.completions.create, not responses.create
  • AgentConfig.to_agent() keeps api_format, endpoint, deployment, api_version

Helm

  • helm/kagent/tests/modelconfig_test.yaml — rendered CR has apiFormat: responses when values set it.

Commands:

go test ./adk/pkg/internal/azureai/ ./adk/pkg/models/ ./core/internal/controller/translator/agent/ -count=1
cd python && uv run pytest packages/kagent-adk/tests/unittests/models/test_openai.py -k azure_openai
# helm unittest for kagent chart modelconfig

Live Azure AI Foundry (opt-in, skipped in CI)

Same skip style as existing kagent tests (KAGENT_E2E_GRPC_TARGET, RUN_UPGRADE_TESTS): no env → skip, so make test does not call Azure and does not spend quota.

Extra gate AZURE_LIVE=1 so a leftover AZURE_OPENAI_API_KEY in the shell does not accidentally bill Foundry during normal unit runs.

export AZURE_LIVE=1
export AZURE_OPENAI_API_KEY=...
export AZURE_OPENAI_ENDPOINT=https://<resource>.services.ai.azure.com/
export AZURE_OPENAI_DEPLOYMENT=gpt-4.1   # optional

go test ./adk/pkg/internal/azureai/ -run Live -count=1 -v
cd python && uv run pytest packages/kagent-adk/tests/unittests/models/test_azure_live.py -s

Recorded against Foundry GPT-4.1 (Go client + Python AgentConfig.to_agent):

Runtime apiFormat Observed URL Reply
Go ADK chatCompletions /openai/deployments/gpt-4.1/chat/completions?api-version=2024-06-01 pong
Go ADK responses /openai/v1/responses (no api-version) pong
Python ADK agent chatCompletions same Chat Completions URL pong
Python ADK agent responses /openai/v1/responses pong

Kind smoke (Go golang-adk Harness + two AgentTemplates) also returned pong for both ModelConfigs. UI/atenet TLS to the worker is a local Substrate cert issue, not this API.

Compatibility

  • Omitting apiFormat = Chat Completions (previous behavior).
  • Breaking changes are acceptable in v1alpha3 alpha; this field is additive.
  • No secrets in the diff. Local keys stay in gitignored .env / values.local.yaml.

Signed-off-by: Jeremie Rouelle <jeremie.rouelle@hotmail.fr>
Complete apiFormat wiring for Azure OpenAI Chat Completions vs
Responses: Python ADK client, Helm values/docs, CRD docs, and an
example ModelConfig.

Unit tests capture the two HTTP shapes. Opt-in Foundry live tests
run only when AZURE_LIVE=1 so CI never calls Azure.

Signed-off-by: Jeremie Rouelle <jeremie.rouelle@hotmail.fr>
@github-actions github-actions Bot added the enhancement New feature or request label Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support apiFormat responses for AzureOpenAI ModelConfig

1 participant