Skip to content

Add Azure OpenAI as a first-class LLM and embedding provider - #3

Merged
ShreeBohara merged 1 commit into
mainfrom
feat/azure-openai-provider
Aug 9, 2026
Merged

Add Azure OpenAI as a first-class LLM and embedding provider#3
ShreeBohara merged 1 commit into
mainfrom
feat/azure-openai-provider

Conversation

@ShreeBohara

Copy link
Copy Markdown
Owner

Both factories advertised multi-provider support behind real ABCs (BaseLLM, BaseEmbeddings) but only ever constructed public-OpenAI clients. This adds azure_openai to each.

Approach

Target Azure's v1 OpenAI-compatible surface (<endpoint>/openai/v1) and reuse the standard AsyncOpenAI client rather than AsyncAzureOpenAI — the openai SDK's own README warns the Azure client's static types "can be incorrect". So the Azure branch is a different base_url and a deployment name, not a second client implementation.

azure_openai_base_url() normalises the endpoint idempotently: bare host, trailing slash, /openai, or an already-complete /openai/v1 all produce the same result.

The two divergences that would otherwise have been silent

/models returns 404 on some Azure configurations. There it enumerates deployments, and not every setup exposes it. health_check now treats 404 as reachable — the endpoint answered, so credentials and networking are fine — while still failing on 401/403 and on unexpected statuses. Without this, /api/health would report a perfectly working Azure deployment as degraded.

tiktoken cannot resolve an encoding from a deployment name. tokenizer_model is now passed separately. I want to be precise about this one rather than overclaim: the old bare except KeyError: cl100k_base was accidentally correct, because every current OpenAI embedding model resolves to cl100k_base anyway —

text-embedding-3-small  -> cl100k_base
text-embedding-3-large  -> cl100k_base
text-embedding-ada-002  -> cl100k_base
fallback used by old code -> cl100k_base

So this is a latent correctness fix plus a warning where there was silence, not a live bug fix. It would have mattered on an o200k_base embedding model or a non-OpenAI base_url.

Two fixes Azure made reachable

  • openai_embedding_dimensions is configurable. It was hardcoded to 1536 while the model was configurable, so a text-embedding-3-large deployment (3072) only failed when Chroma rejected the insert.
  • The unknown-provider branch now raises. It previously fell back to OpenAI whenever a key happened to be set, dropping all seven rate-limit/batching/pacing arguments — so a typo in EMBEDDING_PROVIDER silently produced a differently-behaving client with no error anywhere. llm/factory.py already raised; these now match.

Scope

api_key accepts str | Callable[[], str] so an Entra token provider can be passed without either class knowing how the credential is obtained. Nothing supplies one yet — auth is API-key only, and the docs say so. I'd rather ship that honestly than imply managed identity works.

Verification

Gate Result
New unit tests 22 (URL normalisation, factory wiring, missing-config errors, tokenizer resolution, dimensions, all 5 health-check branches)
Full suite 111 passed, 0 failed (was 89)
ruff check src tests clean
Azure from env alone correct base_url, deployment-as-model, tokenizer, dimensions
Default OpenAI path unchanged — api.openai.com/v1, gpt-4o, 1536
App import 41 routes

Config coverage checked programmatically — all six new variables exist in config.py, docker-compose.yml (55 forwarded) and .env.example.

Not verified: no calls were made against a real Azure resource. The wiring is asserted at construction, which is where the divergences live, but a live smoke test against an actual deployment is still worth doing before relying on it.

🤖 Generated with Claude Code

Both factories advertised multi-provider support behind real ABCs but only ever
constructed public-OpenAI clients. This adds azure_openai to each.

Approach: target Azure's v1 OpenAI-compatible surface (<endpoint>/openai/v1) and reuse
the standard AsyncOpenAI client rather than AsyncAzureOpenAI, whose static types the
openai SDK's own README warns "can be incorrect". So the Azure branch is a different
base_url and a deployment name, not a second client implementation.

- config.py: azure_openai_endpoint / _api_key / _deployment / _embedding_deployment /
  _tokenizer_model, plus azure_openai_base_url() which normalises the endpoint to
  /openai/v1 idempotently (accepts bare host, trailing slash, or an already-complete
  URL).
- llm/factory.py + embeddings/factory.py: azure_openai branch, failing fast with a
  named variable when endpoint / key / deployment are missing.
- openai_llm.py + openai_embeddings.py: api_key widened to str | Callable[[], str] so
  an Entra token provider can be passed without either class knowing how the
  credential is obtained. Nothing here supplies one yet -- auth is API-key only.
- requirements.txt: openai>=1.106.0, the floor Microsoft documents for the v1 surface
  and callable token providers, and the first version exporting the error classes the
  health check now discriminates on. Was >=1.12.0.

Two Azure divergences that would otherwise be silent:

- health_check no longer treats a missing /models route as unhealthy. On Azure that
  route enumerates *deployments* and some configurations omit it entirely; a 404 means
  the endpoint answered, so credentials and networking are fine. 401/403 and unexpected
  statuses still fail. Previously /api/health would have reported a working Azure
  deployment as degraded.
- openai_embeddings.py takes tokenizer_model separately, because tiktoken resolves an
  encoding from a model id and on Azure `model` is a deployment name. Note honestly
  that the old bare `except KeyError: cl100k_base` was *accidentally* correct: every
  current OpenAI embedding model resolves to cl100k_base anyway. So this is a latent
  correctness fix plus a warning where there was silence, not a live bug fix. It would
  have mattered on an o200k_base embedding model or a non-OpenAI base_url.

Also, because Azure makes them reachable:

- openai_embedding_dimensions is now configurable and threaded through both factories.
  It was hardcoded to 1536 while the model was configurable, so a text-embedding-3-large
  deployment (3072) only failed when Chroma rejected the insert.
- embeddings/factory.py's unknown-provider branch now raises instead of falling back to
  OpenAI whenever a key happened to be set. That fallback dropped all seven rate-limit,
  batching and pacing arguments, so a typo in EMBEDDING_PROVIDER silently produced a
  differently-behaving client with no error. llm/factory.py already raised.

docker-compose.yml forwards the six new variables (55 total), and .env.example plus
docker/README.md document deployment-names-not-model-ids, the tokenizer requirement,
the dimensions match, and that Entra is not wired up.

Verified: 22 new unit tests covering URL normalisation, factory wiring, missing-config
errors, tokenizer resolution, dimensions and all five health-check branches; 111 tests
pass (was 89); ruff clean; both providers construct correctly from environment alone
and the default OpenAI path is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codebaseqa-web Ready Ready Preview Aug 9, 2026 7:48pm

@ShreeBohara
ShreeBohara merged commit c0e68aa into main Aug 9, 2026
4 checks passed
@ShreeBohara
ShreeBohara deleted the feat/azure-openai-provider branch August 9, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant