Skip to content

Config: custom provider under "providers" is accepted but silently drops npm and options.baseURL (v2.0.32) #56816

Closed as not planned

Description

@a23sd4

Summary

Declaring a custom provider under the current top-level providers key is accepted by the config loader, and the provider's models are registered (they appear in opencode models and in the model picker), but the two fields that actually make the provider usable - npm and options.baseURL - are silently dropped. Every request against such a model then fails in under two seconds with an empty package name.

Declaring the same provider under the deprecated singular provider key works with an otherwise identical config.

No warning, no config error, nothing at startup. The only visible symptom appears at request time, and it points away from the cause.

Environment

  • opencode v2.0.32
  • Windows, PowerShell 5.1

Repro

opencode.json in an otherwise empty project:

{
  "$schema": "https://opencode.ai/config.json",
  "model": "example-provider/some-model",
  "providers": {
    "example-provider": {
      "npm": "@ai-sdk/openai-compatible",
      "env": ["EXAMPLE_API_KEY"],
      "options": {
        "baseURL": "https://example.test/v1",
        "timeout": 480000,
        "chunkTimeout": 180000
      },
      "models": {
        "some-model": {
          "name": "Some Model (example provider)",
          "tool_call": true,
          "limit": { "context": 1048576, "output": 65536 }
        }
      }
    }
  }
}

With EXAMPLE_API_KEY set in the environment:

  1. opencode models --standalone - lists example-provider/some-model. Looks configured.
  2. opencode debug config - the provider's npm and options.baseURL are absent/empty in the resolved document.
  3. opencode run --standalone -m example-provider/some-model "hi" - fails:
provider.no-route: Unsupported package for example-provider/some-model:

Note the trailing colon and nothing after it: an empty package name.

  1. Change the single key from "providers" to "provider" and repeat step 3 - the request succeeds.

Evidence that the key is the cause

Same config file, one variable changed at a time:

Variant Result
"provider" (singular, deprecated) request succeeds
"providers" + npm at provider level fails, empty package name
"providers" + npm under each models.<id>.provider fails identically
"providers" + api instead of npm fails identically
"providers" + per-model provider.npm only fails identically
control: config round-tripped through ConvertTo-Json -Depth 20, singular key kept request succeeds

The control rules out serialisation and field placement: identical bytes apart from the key name.

Expected

Either

  • "providers" wires the provider up the same way "provider" does, or
  • declaring a provider that cannot be resolved produces a startup/config error and is not advertised in opencode models.

Registering the models while discarding the connection fields is the worst of the options, because every diagnostic (opencode models, the picker, the config dump) reports success.

Possible cause

providers may be intended as an override layer for already-registered providers, while provider was the V1 registration key. If so, the schema does not convey that distinction: both accept npm, env, options and models, so a new provider declared under providers parses cleanly and does nothing.

A narrow check that would distinguish the two: does an entry in providers for a provider that is not already registered get a package from npm? Under the current behaviour it does not.

Activity

  1. opencode-agent commented on Oct 18, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on v2.0.32, and it still happens on the latest dev (da9d436).

    What I ran (the script below does the same, against a local mock OpenAI-compatible server):

    • Under "providers", npm, options.baseURL and the model's tool_call are dropped without any warning. opencode models still lists example-provider/some-model, opencode debug config shows the provider with only env and models, and opencode run fails with Unsupported package for example-provider/some-model:.
    • Under "provider" (singular), the same entry is converted to the v2 field names automatically, and the request succeeds.

    The v2 providers schema uses different field names: package instead of npm, settings instead of options, and capabilities.tools instead of tool_call. Unknown keys are currently ignored without a warning. Until this is addressed, this form worked for me under providers:

    "example-provider": {
      "package": "aisdk:@ai-sdk/openai-compatible",
      "env": ["EXAMPLE_API_KEY"],
      "settings": { "baseURL": "https://example.test/v1", "timeout": 480000, "chunkTimeout": 180000 },
      "models": { "some-model": { "name": "Some Model", "capabilities": { "tools": true }, "limit": { "context": 1048576, "output": 65536 } } }
    }

    Your point still holds: the config should either warn about or migrate the v1 field names instead of registering a provider that can't work. The maintainers will look at that part.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #56816: v1 field names (npm, options.baseURL) under v2 `providers` are silently dropped.
    # Usage: OC="opencode" ./repro.sh   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    OC=${OC:-opencode}
    # Mock OpenAI-compatible endpoint so the "provider" control can succeed
    python3 - <<'PY' &
    import http.server, json
    class H(http.server.BaseHTTPRequestHandler):
        def do_POST(self):
            self.rfile.read(int(self.headers.get('content-length',0)))
            body = ('data: '+json.dumps({"id":"x","object":"chat.completion.chunk","created":0,"model":"some-model","choices":[{"index":0,"delta":{"role":"assistant","content":"hello from mock"},"finish_reason":None}]})+'\n\n'
                   +'data: '+json.dumps({"id":"x","object":"chat.completion.chunk","created":0,"model":"some-model","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}})+'\n\ndata: [DONE]\n\n')
            self.send_response(200); self.send_header('content-type','text/event-stream'); self.end_headers(); self.wfile.write(body.encode())
        def log_message(self,*a): pass
    http.server.HTTPServer(('127.0.0.1',18765),H).serve_forever()
    PY
    MOCK=$!
    trap 'kill $MOCK' EXIT
    sleep 1
    export EXAMPLE_API_KEY=dummy
    run() { # $1 = key name
      d=$(mktemp -d); cd "$d"; git init -q
      cat > opencode.json <<JSON
    {
      "\$schema": "https://opencode.ai/config.json",
      "model": "example-provider/some-model",
      "$1": {
        "example-provider": {
          "npm": "@ai-sdk/openai-compatible",
          "env": ["EXAMPLE_API_KEY"],
          "options": { "baseURL": "http://127.0.0.1:18765/v1" },
          "models": { "some-model": { "name": "Some Model", "tool_call": true, "limit": { "context": 100000, "output": 8000 } } }
        }
      }
    }
    JSON
      echo "===== key: \"$1\" ====="
      echo "--- models:"; $OC models --standalone 2>&1 | grep -i example
      echo "--- debug config (provider section):"; $OC debug config 2>&1 | grep -A12 '"example-provider"' | head -20
      echo "--- run:"; timeout 60 $OC run --standalone -m example-provider/some-model "hi" 2>&1 | tail -5
      cd /; rm -rf "$d"
    }
    run providers
    run provider

    Run it: bash repro.sh

  2. argszero commented on Oct 18, 2026

    @argszero
    Contributor

    I'd like to work on this. I read the code path and can confirm the mechanism, which is narrower than it first looks.

    Why provider works and providers does not. Both keys are real and both are read by the same normalizer, but by different routes:

    • packages/core/src/config/normalize.ts:168 sends the legacy singular provider through migrateProviders, which runs each entry through ConfigProviderV1.Info and then ConfigMigrateV1.migrateProvider. That migration is where the V1 spellings are translated: packages/core/src/v1/config/migrate.ts:124 maps npm to package, and :126-128 maps options (and api) onto settings / headers / body.
    • packages/core/src/config/normalize.ts:169 decodes the new providers key straight against ConfigProvider.Info, whose fields are package, settings, headers, body, models, env, name, canonical, compatibility (packages/schema/src/config/provider.ts). There is no npm and no options in that struct.

    The decoder is configured with onExcessProperty: "ignore" (normalize.ts:44), so npm and options.baseURL are dropped without a word — which is exactly the "silently" in the title. The models are still registered because models is a valid field, which is why opencode models advertises a provider that cannot route, and why the failure surfaces later as an empty package name from packages/core/src/model-resolver.ts:37 (Unsupported package for <provider>/<model>: — package undefined).

    What I intend to change. Fix the silence for the providers key the way the legacy key already handles its unsupported fields: normalize.ts:605 diagnoseProviderUnsupported reports V1-only keys through the existing unsupported diagnostic kind, and providers currently gets no equivalent. I would report the V1 spellings on the native path as well — npm at provider level pointing at package, options pointing at settings/headers/body, and the per-model provider.npm — so the warning names the replacement instead of the field vanishing. Diagnostics are already surfaced as log warnings by packages/core/src/config.ts:123, so the user sees the reason at load time rather than an empty package name at request time.

    That changes reporting only: no config field is added, removed or re-interpreted, and no provider that resolves today stops resolving.

    Where I would want a maintainer's word. Your "Expected" section offers two other outcomes, and both are config-surface decisions rather than bug fixes, so I do not want to pick one unasked:

    1. accepting npm / options under providers as aliases of package / settings, which would make the V1 spelling work everywhere and make the migration's mapping redundant for new files; or
    2. rejecting the entry outright at load and not advertising the provider's models.

    If either is preferred over the diagnostic, say so and I will build that instead.

  3. rekram1-node commented on Oct 22, 2026

    @rekram1-node
    Contributor

    Thanks for the report. The config is mixing V1 and V2 shapes. providers (plural) is the V2 key and expects V2 field names: package instead of npm, settings instead of options, and capabilities.tools instead of tool_call. Under providers, npm, options and tool_call aren't valid fields, so the provider ends up with no package or endpoint. The schema at https://opencode.ai/config.json flags them as not allowed there.

    Either keep the V1 shape under the singular provider key, or rewrite the entry in the V2 shape. See https://opencode.ai/v2/docs/providers#custom for the format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions