Skip to content

feat(core): discover Azure deployments - #50053

Merged
neriousy merged 8 commits into
v2from
azure-discovery
Oct 1, 2026
Merged

neriousy merged 8 commits into
v2from
azure-discovery

Conversation

@neriousy

@neriousy neriousy commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Issue for this PR

Closes #

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Azure only serves a model through a deployment, but OpenCode showed the whole Azure catalog (89 models) and sent the catalog ID as the deployment name. This PR lists the resource's deployments and shows those instead.

What it fixes

Before After
All 89 catalog models are listed; picking one that isn't deployed fails with a 404 from Azure Only the resource's deployments are listed
A deployment named differently from its model (prod → gpt-5-mini) needs a manual modelID mapping in config It appears as azure/prod with gpt-5-mini's limits and costs
With an API key, the resource name from the connect form never reaches Foundry base URLs (https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/...), so DeepSeek, Grok, Llama and the other Foundry models fail with an unresolved variable The connection's resource fills them in
When config and the connection named different resources, OpenAI models used the connection's and Foundry models used config's Everything follows the connection, matching the model resolver

Startup

Startup does no network calls and never runs az. The plugin reads the stored credential and serves the full catalog; discovery runs in a background fiber and narrows the list when it answers. On v2, loading the plugin resolved the connection, so an expired Azure CLI token was refreshed through az before startup could continue.

Measured against a real AIServices resource, with the real az and the real 89-model catalog, both sides on the same v2 commit. "Ready" is the time until the plugin has loaded, which startup waits for. The deployment times come from one fresh process per run, in shuffled order, so every run starts with cold connections like a real startup; medians of 6 runs:

Connection v2 ready This PR ready Deployments listed (this PR, background) Where the background time goes
none 1 ms 1 ms – –
API key 4 ms 5 ms 195 ms one request to the resource, 180 ms
Azure CLI, valid token 5 ms 5 ms 1469 ms az management token 171 ms, Resource Graph 428 ms, deployment list 849 ms
Azure CLI, expired token 197 ms (az refresh) 5 ms 1538 ms the same plus the az refresh, 171 ms

The two Azure Resource Manager requests vary by a few hundred milliseconds between runs, which hides most of the extra refresh in the medians; the az calls themselves stay at 167–176 ms.

gantt
  title Azure CLI with an expired token (one run)
  dateFormat x
  axisFormat %S.%L s
  section v2
  az token refresh, startup waits        :crit, 0, 197
  section This PR
  plugin loads, catalog shown            :done, 0, 5
  az token refresh                       :5, 177
  az management token                    :178, 347
  Resource Graph                         :347, 966
  deployment list, then the list narrows :970, 1488
Loading

az answered from its own token cache here. When that cache has expired too, az refreshes over the network, and on v2 startup waits for all of it, up to the 10 s command timeout. With this PR that time only delays the background list. If discovery fails, or settings.baseURL is custom, the full catalog stays. Tests assert that no request and no az call happen before the plugin finishes loading.

There is no polling. Discovery runs at startup, after connecting, and after switching accounts, so a deployment added later appears after a restart or reconnect.

How it works

flowchart TD
  T["startup · connect · account switch"] --> RB["rebind (local, no network)<br/>read the active connection and its resource<br/>tie the provider to that connection"]
  RB --> D{"credential"}
  D -- "Azure CLI" --> RG["Resource Graph: resource ID<br/>(cached per resource)"]
  RG --> MG["management API: every deployment page<br/>(nextLink must stay on management.azure.com)"]
  MG -- "fails" --> LEG
  D -- "API key" --> LEG["resource's own list<br/>/openai/deployments?api-version=2022-12-01"]
  MG -- "ok" --> P
  LEG -- "ok" --> P{"still the active connection?"}
  LEG -- "fails" --> KEEP["log a warning, keep the last list<br/>(or the catalog)"]
  P -- "yes" --> PUB["show the deployed models"]
  P -- "no" --> DROP["discard"]
Loading

The management API is Azure's documented deployment list, but Azure Resource Manager only accepts Entra tokens. API keys therefore use the data-plane list, which only exists in 2022-12-01; later versions dropped /deployments, and /openai/v1/models lists models the resource can deploy rather than its deployments. Links to the Azure docs are next to each caveat in the code.

Switching accounts

sequenceDiagram
  participant U as User
  participant P as Azure plugin
  participant A as discovery (account A)
  participant B as discovery (account B)
  Note over A: still waiting on Azure
  U->>P: switch to account B
  P->>P: rebind locally, reload the provider
  Note over P: Azure is back immediately with<br/>the full catalog for B
  P--xA: interrupt
  P->>B: start
  B->>P: B's deployments
Loading

The previous account's list is never shown for the new one. The interrupted discovery cannot publish, and every discovery checks that its connection is still active before publishing.

Deployments to models

The ID is the deployment name in lowercase; Azure compares names without case, and requests send the name exactly as Azure returned it. Limits, costs, and capabilities come from the catalog model the deployment deploys, or, when Azure spells it differently (gpt-4 for GPT-4 Turbo, gpt-35-turbo), from the catalog model the deployment is named after.

Deployment (name → Azure model) Model in OpenCode
gpt-5-mini → gpt-5-mini azure/gpt-5-mini, "GPT-5 Mini"
prod → gpt-5-mini azure/prod, "GPT-5 Mini (prod)"
GPT-5-Nano → gpt-5-nano azure/gpt-5-nano
gpt-4-turbo → gpt-4 azure/gpt-4-turbo with GPT-4 Turbo's details
ft-legal → gpt-4o-mini.ft-123 not shown; a warning names it, and it can be configured explicitly

Models configured explicitly are always kept.

Compatibility

  • Stored credentials keep their format, so connections need no migration or reconnect.
  • A session stores azure/<id>. Any deployment named after a catalog ID keeps that ID, so every session that worked before keeps working; only models without any deployment disappear, and those already failed with a 404.
  • The resource name saved with the connection now wins over settings.resourceName in config. This only matters if config was changed to another resource after connecting; reconnecting picks it up.

How did you verify your code works?

  • bun test test/plugin/provider-azure.test.ts in packages/core: 29 tests against a local Bun.serve fake of Azure, repeated to check the switch timing. bun run check passes.
  • Against a real AIServices resource with gpt-5-mini and DeepSeek-V4-Flash deployed:
    • The legacy list with an API key returns both, including the non-OpenAI deployment.
    • The real plugin narrowed the 89-model catalog to exactly those two, with an API key and with the Azure CLI through the management API.
    • Azure accepted DeepSeek-V4-Flash, deepseek-v4-flash, and DEEPSEEK-V4-FLASH as the deployment name.
  • Not covered live: an Azure CLI identity without management read access (the legacy fallback), several resources or tenants, and deployments whose Azure model name differs from the catalog.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Discover Azure resources and deployments in the background with stable model IDs, paginated inventories, and connection-bound snapshots. Validate resource access before saving new API-key or Azure CLI connections, report actionable failures, and preserve active credentials on rejected attempts.
@neriousy neriousy changed the title feat(core): discover and validate Azure resources feat(core): discover Azure deployments Sep 28, 2026
@neriousy
neriousy merged commit fbe8a9f into v2 Oct 1, 2026
14 checks passed
@neriousy
neriousy deleted the azure-discovery branch October 1, 2026 18:42
1056674754 added a commit to 1056674754/opencode that referenced this pull request Oct 5, 2026
Upstream v2.0.21 (014614d lineage, merged at 835531c) -> v2.0.22
(527f0b9), 76 commits / 1759 files. First normal merge on the v2 line —
merge base f46fa72 exists (same re-rooted history). Staged delta vs
pre-merge HEAD matches the upstream tag-to-tag diff modulo branding.

Content highlights:
- fix(ai): report mid-stream connection loss instead of a decode error
  (anomalyco#52634); isolate Groq and Vertex metadata keys (anomalyco#52598); enable Alibaba
  chat prompt caching (anomalyco#52612); classify Together input token rejections as
  context overflow (anomalyco#52133).
- fix(core): default provider header/chunk timeouts to five minutes
  (anomalyco#49229); accept partial model capabilities in native provider config;
  Azure deployments discovery (anomalyco#50053); skip automatic copies of directly
  read instructions (anomalyco#52382).
- feat(cli): 10-minute update checks (anomalyco#52552); feat(app): last-turn changes
  in review panel (anomalyco#51640).
- Large refactor pass deleting unused v1-era helpers and errors (bulk of
  the -86k lines), ACP SDK 1.6.0, docs/tests.

Conflicts resolved: 37 package.json version bumps (rebranded 2.0.22-sscity)
+ bun.lock (upstream, rebranded via bun install; 2 packages installed) +
one hunk in core/src/session/runner/retry.ts — upstream added a
MAX_TIMEOUT_RETRIES=3 cap with its own `timeouts` counter alongside the
fork's contentPolicyAttempts cap; both counters and both gates are kept.
The fork's other port files (zaiGlm scrub, cyber_policy codes, schema
fallbacks, normalize, title chain) were untouched upstream and merged
clean. Functional markers verified post-merge. Tests: ai
zai/openai-chat/provider-error/compatible-chat 150 pass / 0 fail; core
session-error/title/prompt/normalization 99 pass / 1 fail
(session-revert rename case — pre-existing on pristine v2.0.21, verified
in the port round). Full workspace typecheck green.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Ichinose-Kazuki pushed a commit to Ichinose-Kazuki/opencode that referenced this pull request Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant