Introduction
I had an Azure model deployment, an endpoint, and an API key. Getting that model into OpenCode looked like a configuration task: register a provider, paste in the connection details, select the model.
Then came “Resource not found.” After a provider change, a more useful error appeared:
Unsupported parameter: ‘max_tokens’ is not supported with this model. Use ‘max_completion_tokens’ instead.
The breakthrough was a tiny request outside OpenCode. With the right parameter, the model replied: “Hi!”
That response narrowed the problem. The deployment could answer a request. The remaining question was what the coding agent’s provider adapter was sending differently.
This post follows that investigation, including the detours I would skip next time. It also gives the setup sequence I wish I had followed at the start. The direct Chat Completions test succeeded, and we applied a revised OpenCode configuration. The session record stops before a successful end-to-end retest of that final configuration, so I have kept that boundary explicit.
What you will get: a configuration example, two API smoke tests, an explanation of the token-parameter mismatch, and a troubleshooting checklist that starts with evidence rather than another SDK installation.
1. Separate the names from the connection
This happened while I was working in the EntraGuard project on macOS. OpenCode already had an Azure provider, and I was adding a custom provider called foundry. The deployment identifier used in this session was gpt-6-astra. Use your own deployed model’s identifier; that name is part of this case study, not a promise of availability in every Azure account.
Before touching the config, collect four things:
- The endpoint shown for your Microsoft Foundry resource.
- The exact deployment name that the API expects in model.
- A credential for that same resource.
- The API route you intend to use: Responses or Chat Completions.
The local provider name, the Azure resource name, and the deployment name are different things. In foundry/gpt-6-astra, foundry is an OpenCode label. It does not create or discover an Azure deployment.
Figure 1. The adapter builds the request. A model appearing in the picker only confirms the registration step.
Use the API root as baseURL
For this integration, the URL shapes were:
API root:
https://YOUR-RESOURCE.services.ai.azure.com/openai/v1
Responses route: /responses
Chat Completions route: /chat/completions
With AI-sdk/openai, baseURL is the root ending in /openai/v1, not the complete URL ending in /responses. The adapter appends the operation path. Microsoft documents both the services.ai.azure.com and openai.azure.com host formats for the v1 API. A dated api-version parameter is not required for v1 GA.
We had two similarly named Azure resources in the investigation. Confirming the correct one was worthwhile. It did not, by itself, explain every error.
2. Ask Azure directly before asking OpenCode
This was the most useful test of the session. Removing the agent let me control the route, credential, model name, and body in one place.
The following examples use Bash, curl, and a resource with an existing model deployment. Replace the two placeholders first. Enter the key at the prompt rather than putting it in the article or a shared config.
export FOUNDRY_BASE_URL=”https://YOUR-RESOURCE.services.ai.azure.com/openai/v1″
export FOUNDRY_DEPLOYMENT=”YOUR-DEPLOYMENT”
read -r -s -p “Foundry API key: ” FOUNDRY_API_KEY
printf ‘\n’
export FOUNDRY_API_KEY
Test Chat Completions
curl –silent –show-error –fail-with-body \
“${FOUNDRY_BASE_URL}/chat/completions” \
-H “Content-Type: application/json” \
-H “api-key: ${FOUNDRY_API_KEY}” \
–data-binary @- <<EOF
{
“model”: “${FOUNDRY_DEPLOYMENT}”,
“messages”: [
{“role”: “user”, “content”: “Say hi in one word”}
],
“max_completion_tokens”: 256
}
EOF
The first test in our session used max_tokens and was rejected. Changing the field to max_completion_tokens produced “Hi!” in choices[0].message.content.
Our successful request used a limit of 10. I have used 256 in this reusable example because reasoning can consume the completion budget before visible text appears. Neither value is a recommended limit for a real coding task.
A successful direct request proves that this route, credential, deployment, and request body work together. It does not yet prove streaming or tool use inside the agent.
If the request fails, keep the error body. A model-parameter error is a different clue from a connection failure or a missing deployment. Treating them as the same problem sends you back to checking the key unnecessarily.
3. Check the API route the new adapter will use
There was one detail I initially gave too little attention: Responses and Chat Completions are different APIs.
The current AI SDK OpenAI provider uses Responses by default. Its explicit .chat() factory uses Chat Completions. That means changing an adapter can change more than a field name; it can change the route and body structure too.
Figure 2. Match the field to the API. These are request examples, not interchangeable OpenCode configuration options.
Test Responses separately
Because the revised setup uses AI-sdk/openai, this is the next direct check I would run:
curl –silent –show-error –fail-with-body \
“${FOUNDRY_BASE_URL}/responses” \
-H “Content-Type: application/json” \
-H “api-key: ${FOUNDRY_API_KEY}” \
–data-binary @- <<EOF
{
“model”: “${FOUNDRY_DEPLOYMENT}”,
“input”: “Say hi in one word”,
“max_output_tokens”: 256,
“store”: false
}
EOF
Look for generated text in the response’s output items. Do not use a Chat Completions parser that expects choices[0].message.content.
This Responses test is included as a recommended verification step. It was not captured as a successful test in the original session. That matters: the supplied endpoint ended in /responses, but the successful request we actually recorded went to /chat/completions.
Microsoft recommends Responses for Azure OpenAI models. Still, test the route for your actual deployment instead of inferring support from a successful call to another endpoint.
4. Register the provider in OpenCode
The global configuration we edited was ~/.config/opencode/opencode.jsonc. OpenCode accepts JSON and JSONC, and merges global and project configuration. Use provider, singular, and put connection settings under options.
Here is a sanitized version of the configuration we ended with. It replaces the literal key and resource URL with environment references. Merge the foundry entry into your existing provider object rather than replacing your whole file.
{
“$schema”: “https://opencode.ai/config.json”,
“provider”: {
“foundry”: {
“npm”: “@ai-sdk/openai”,
“name”: “Azure AI Foundry”,
“options”: {
“baseURL”: “{env:FOUNDRY_BASE_URL}”,
“apiKey”: “{env:FOUNDRY_API_KEY}”,
“headers”: {
“api-key”: “{env:FOUNDRY_API_KEY}”
}
},
“models”: {
“gpt-6-astra”: {
“id”: “{env:FOUNDRY_DEPLOYMENT}”,
“name”: “GPT-6 Astra (Foundry)”
}
}
}
}
}
The model-map key is the local selector. id is what the adapter sends as the deployment name. If you are deploying another model, rename the key and display name too. In this example, the local selection remains foundry/gpt-6-astra.
The shell variables from step 2 must be available to the OpenCode process. Launching from that same terminal is the simplest way to check this. An unset environment reference becomes an empty string in OpenCode.
Why both apiKey and api-key?
This reproduces the final configuration we applied. The OpenAI adapter uses apiKey for its Authorization header, while headers adds the explicit Azure api-key header used in our direct test. Adding api-key does not remove Authorization.
We did not establish that both headers were necessary. Microsoft documents v1 use with standard OpenAI clients, so my earlier assumption that an OpenAI-style client could never authenticate to Azure was too broad. The explicit header was a compatibility choice, not a proven authentication fix.
5. Why the provider change mattered
Our first custom-provider attempt used AI-sdk/azure and returned “Resource not found.” We suspected URL construction, but we did not capture enough request detail to prove the exact cause. I would not turn that into advice that the Azure adapter cannot work with Foundry.
We then tried AI-sdk/openai-compatible. OpenCode recognized the model, but the model request failed with the max_tokens error.
Inspecting the compatible adapter version installed during the investigation showed the important line:
max_tokens: maxOutputTokens
That is reasonable for many compatible Chat Completions services. It was wrong for this deployed model, which explicitly required max_completion_tokens.
The OpenAI adapter source we inspected had model-aware handling. For recognized reasoning models on its Chat Completions path, it moved the limit into max_completion_tokens and removed max_tokens. The inspected version also recognized the GPT-6 model family.
That made AI-sdk/openai a better candidate for this integration. On its default Responses path, however, the relevant field is max_output_tokens. The useful fix was choosing an adapter that understood the API and model, not globally renaming a JSON property.
Two shortcuts I would avoid
Adding a second token field without removing the first. If the outgoing request still contains max_tokens, the service can still reject it. A config option named maxTokens is not a generic field-renaming switch.
Assuming the latest globally installed package is what OpenCode uses. We installed SDK packages globally while troubleshooting. That did not demonstrate which version the OpenCode binary loaded. Current OpenCode source includes bundled mappings for both OpenAI adapters. A global npm installation is not an upgrade mechanism for a bundled adapter.
SDK inspection was useful, but it was evidence about the inspected package. It was not a packet capture from the running OpenCode process. If the same error persists after restarting, check the selected provider and the actual request path before assuming a conversion ran.
“OpenAI-compatible” describes a broad interface. It does not mean every model accepts every optional parameter.
6. Validate the agent in small steps
With the environment set and the config saved, check registration first:
opencode models foundry
In our session, the model appeared as foundry/gpt-6-astra. That confirmed OpenCode could register the provider. It was not an inference test.
Restart OpenCode from the configured terminal, open /models, and select that model explicitly. This is easy to miss when another Azure model is still the default.
Start with a small prompt:
Reply with exactly: Foundry connection OK
Do not use tools.
After a successful text response, try a short follow-up. Then test one read-only tool action in a small project, such as asking the agent to read the README and summarize it. That checks more of the agent workflow than a one-word response does.
What happened to the CLI test?
I also attempted:
opencode run “Say hello in one word” –model foundry/gpt-6-astra
It returned Session not found. We did not establish the cause. OpenCode documents run as a standalone non-interactive command; an existing TUI session is not a general prerequisite. My initial explanation that it simply needed an active session was incorrect.
That failed command also did not tell us whether the TUI would work. For a fresh investigation, I would reproduce it from a normal terminal, record the OpenCode version, and inspect the local session/configuration behavior separately.
Figure 3. Where the recorded investigation ended. A saved configuration is a milestone, not a successful agent response.
Conclusions
Here is the short version of the investigation. The right-hand column is what I would check first if I encountered the same symptoms again.
|
Obstacle |
What we learned |
Better next move |
|
Config was not loading correctly |
The initial file needed syntax and schema-key corrections. |
Fix parsing first. Use provider and options, then check registration. |
|
“Resource not found” |
The Azure-adapter attempt failed, but its exact outgoing URL was not captured. |
Verify host, full path, deployment name, and matching resource credential. |
|
Similar resource names |
It was possible to confuse the working Azure resource with the Foundry target. |
Copy the endpoint and credential from the same resource. |
|
Model visible, request rejected |
Registration worked; the compatible adapter still sent max_tokens. |
Inspect the body and choose a model-aware adapter. |
|
Direct API call succeeded |
max_completion_tokens produced “Hi!” through Chat Completions. |
Test Responses separately if the agent uses that route. |
|
Global SDK installations |
Installation succeeded, but we did not prove OpenCode used those packages. |
Check the application build and bundled provider behavior. |
|
“Session not found” |
The CLI smoke test could not verify inference. Its cause remained open. |
Reproduce the session issue independently. |
|
Final config saved |
JSON validation passed after the provider and header changes. |
Restart, select the provider, then verify text, streaming, and a tool call. |
A note about validating JSONC
We eventually validated the file with a JSON parser because its contents were plain JSON. An earlier validation command failed because the command itself was wrong, not because it had discovered a configuration error.
For a real JSONC file, use a JSONC-aware validator or OpenCode’s own config loading. Do not strip // from the file as a shortcut. That can corrupt a URL such as https://….
OpenCode’s debug config command can help you inspect resolved settings locally. Review its output before sharing it: environment substitution can turn a harmless-looking reference into the actual credential.
What I would do differently next time
I would begin with the direct API request.
Not because curl is a better client, but because it makes the experiment smaller. If the same deployment and credential answer a known request, I can stop changing unrelated settings and focus on what the agent sends.
My sequence would be:
- Confirm the resource endpoint and deployed model name.
- Send a minimal request to the intended API route.
- Register one custom provider with an explicit base URL.
- Confirm the selected adapter and its API family.
- Restart and test a short text response.
- Test streaming and one tool round-trip before calling the integration complete.
I would also keep a small record of each attempt: the adapter, the route, the changed field, and the response. In this session, several changes happened while we were still guessing about authentication and package loading. That made the investigation longer than it needed to be.
The useful result
The strongest finding was specific and reproducible: our deployed model rejected max_tokens on Chat Completions and answered when the request used max_completion_tokens.
The revised OpenCode configuration used AI-sdk/openai, the /openai/v1 API root, and an explicit api-key header. It was saved and parsed successfully. Full-agent confirmation remained the next check in the recorded session.
That may be less tidy than saying one line fixed everything, but it is more useful when somebody else hits the same error. They know which part we proved, which configuration we applied, and what they need to verify in their own environment.
The endpoint’s “Hi!” was the turning point. It gave us a working request to compare against. Next time, I will ask for that first.
Start with a request you can explain. Then add the agent.


