[FEATURE]: Support Model-Level Configuration override Provider-Level, such as baseURL and apiKey #8036

Open
opened 2026-02-16 18:08:58 -05:00 by yindo · 2 comments
Owner

Originally created by @legendtkl on GitHub (Jan 30, 2026).

Originally assigned to: @thdxr on GitHub.

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Summary

This proposal suggests extending opencode's configuration system to support model-level baseURL and apiKey options, allowing different models within the same provider to use different API endpoints and authentication credentials.

I'd like to contribute for this issue.

Motivation

Use Case

When using custom AI providers (e.g., enterprise proxy, multi-region deployments), different models may need to route to different endpoints:

Provider: modelhub-gpt
├── gpt-4o          → https://chat.internal.com/api/v1              (Chat API)
└── gemini-2.0      → https://gemini.internal.com/api/v1            (Different region)

Each endpoint may also require different API keys (e.g., different quota pools, access controls).

Current Limitation

Currently, baseURL and apiKey can only be configured at the provider level:

{
  "provider": {
    "my-provider": {
      "options": {
        "baseURL": "https://api.example.com",  // ← Provider-level only
        "apiKey": "xxx"                         // ← Provider-level only
      },
      "models": {
        "model-a": { "options": { /* cannot override baseURL/apiKey here */ } }
      }
    }
  }
}

Workaround Limitations

Workaround 1: Multiple providers - Create separate provider entries for each endpoint.

  • Problem: Configuration duplication, harder to maintain, confusing UX.

Workaround 2: Plugin-level URL rewriting - Intercept requests in custom fetch and rewrite URLs.

  • Problem: Complex, error-prone URL parsing, hard to maintain, duplicates logic that should be in core.

Proposed Solution

Design Principle

Allow model.options.baseURL and model.options.apiKey to override provider-level values when initializing the SDK.

Code Change

File: packages/opencode/src/provider/provider.ts

Location: getSDK() function (lines 959-1058)

Current code (lines 966-978):

const options = { ...provider.options }

if (!options["baseURL"]) options["baseURL"] = model.api.url
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
if (model.headers)
  options["headers"] = {
    ...options["headers"],
    ...model.headers,
  }

Proposed change:

const options = { ...provider.options }

// NEW: Merge model-level options (model options take precedence)
if (model.options) {
  for (const [key, value] of Object.entries(model.options)) {
    if (value !== undefined) {
      options[key] = value
    }
  }
}

if (!options["baseURL"]) options["baseURL"] = model.api.url
if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
if (model.headers)
  options["headers"] = {
    ...options["headers"],
    ...model.headers,
  }

Configuration Example

After this change, users can configure model-level endpoints:

{
  "provider": {
    "modelhub-gpt": {
      "name": "ModelHub GPT",
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "https://default.api.com/v1",
        "apiKey": "default-key"
      },
      "models": {
        "gpt-4o": {
          "id": "gpt-4o",
          "name": "GPT-4o",
          "options": {
            "baseURL": "https://chat.api.com/v1"
          }
        },
        "gemini-2.0-flash": {
          "id": "gemini-2.0-flash",
          "name": "Gemini 2.0 Flash",
          "options": {
            "baseURL": "https://gemini.api.com/v1",
            "apiKey": "gemini-specific-key"
          }
        }
      }
    }
  }
}

Impact Analysis

Backward Compatibility

Fully backward compatible

  • If model.options is empty or undefined, behavior is unchanged.
  • Existing configurations continue to work without modification.
  • Provider-level baseURL/apiKey remains the default.

SDK Caching

The current SDK caching mechanism (line 980-982) uses a hash of { providerID, npm, options }:

const key = Bun.hash.xxHash32(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options }))

Since model-level options are now merged into options before computing the hash, different models with different baseURL/apiKey will correctly get separate SDK instances. No additional changes needed.

Type Safety

The Model type already supports options: z.record(z.string(), z.any()) (line 572), so no schema changes are required.

Security Considerations

  • API keys in model-level options follow the same security model as provider-level options.
  • No new attack vectors introduced.

Alternative Approaches Considered

Alternative A: Add dedicated model.baseURL and model.apiKey fields

// In Model schema
baseURL: z.string().optional(),
apiKey: z.string().optional(),

Rejected because:

  • Requires schema changes
  • Less flexible than generic options override
  • Inconsistent with existing options pattern

Alternative B: Use model.api.url for baseURL override

The current code already has:

if (!options["baseURL"]) options["baseURL"] = model.api.url

Rejected because:

  • model.api.url serves a different semantic purpose (API metadata)
  • Doesn't solve the apiKey override requirement
  • Conflates configuration with metadata

Alternative C: Handle in plugin custom fetch

Let plugins intercept and rewrite URLs in their custom fetch handler.

Rejected because:

  • Complex URL parsing and reconstruction
  • Error-prone (must handle all URL patterns)
  • Duplicates logic across plugins
  • Core should provide clean configuration mechanism

Implementation Checklist

  • Add model options merging in getSDK() (5 lines)
  • Add unit tests for model-level baseURL override
  • Add unit tests for model-level apiKey override
  • Add unit tests for mixed provider + model options
  • Update documentation

Test Plan

describe("getSDK with model-level options", () => {
  it("should use model baseURL when specified", async () => {
    // Provider: baseURL = "https://provider.com"
    // Model: options.baseURL = "https://model.com"
    // Expected: SDK uses "https://model.com"
  })

  it("should use model apiKey when specified", async () => {
    // Provider: apiKey = "provider-key"
    // Model: options.apiKey = "model-key"
    // Expected: SDK uses "model-key"
  })

  it("should fall back to provider options when model options not specified", async () => {
    // Provider: baseURL = "https://provider.com", apiKey = "provider-key"
    // Model: options = {}
    // Expected: SDK uses provider values
  })

  it("should create separate SDK instances for models with different options", async () => {
    // Model A: options.baseURL = "https://a.com"
    // Model B: options.baseURL = "https://b.com"
    // Expected: Different SDK instances (different cache keys)
  })
})

Conclusion

This proposal provides a minimal, backward-compatible change that enables model-level baseURL and apiKey configuration. The change:

  1. Is minimal - Only 5 lines of code added
  2. Is safe - Fully backward compatible, no breaking changes
  3. Is consistent - Follows existing options pattern
  4. Is complete - Solves both baseURL and apiKey requirements
  5. Is correct - SDK caching works correctly with no additional changes
Originally created by @legendtkl on GitHub (Jan 30, 2026). Originally assigned to: @thdxr on GitHub. ### Feature hasn't been suggested before. - [x] I have verified this feature I'm about to request hasn't been suggested before. ### Describe the enhancement you want to request ## Summary This proposal suggests extending opencode's configuration system to support model-level `baseURL` and `apiKey` options, allowing different models within the same provider to use different API endpoints and authentication credentials. I'd like to contribute for this issue. ## Motivation ### Use Case When using custom AI providers (e.g., enterprise proxy, multi-region deployments), different models may need to route to different endpoints: ``` Provider: modelhub-gpt ├── gpt-4o → https://chat.internal.com/api/v1 (Chat API) └── gemini-2.0 → https://gemini.internal.com/api/v1 (Different region) ``` Each endpoint may also require different API keys (e.g., different quota pools, access controls). ### Current Limitation Currently, `baseURL` and `apiKey` can only be configured at the **provider level**: ```json { "provider": { "my-provider": { "options": { "baseURL": "https://api.example.com", // ← Provider-level only "apiKey": "xxx" // ← Provider-level only }, "models": { "model-a": { "options": { /* cannot override baseURL/apiKey here */ } } } } } } ``` ### Workaround Limitations **Workaround 1: Multiple providers** - Create separate provider entries for each endpoint. - Problem: Configuration duplication, harder to maintain, confusing UX. **Workaround 2: Plugin-level URL rewriting** - Intercept requests in custom fetch and rewrite URLs. - Problem: Complex, error-prone URL parsing, hard to maintain, duplicates logic that should be in core. ## Proposed Solution ### Design Principle Allow `model.options.baseURL` and `model.options.apiKey` to override provider-level values when initializing the SDK. ### Code Change **File:** `packages/opencode/src/provider/provider.ts` **Location:** `getSDK()` function (lines 959-1058) **Current code (lines 966-978):** ```typescript const options = { ...provider.options } if (!options["baseURL"]) options["baseURL"] = model.api.url if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key if (model.headers) options["headers"] = { ...options["headers"], ...model.headers, } ``` **Proposed change:** ```typescript const options = { ...provider.options } // NEW: Merge model-level options (model options take precedence) if (model.options) { for (const [key, value] of Object.entries(model.options)) { if (value !== undefined) { options[key] = value } } } if (!options["baseURL"]) options["baseURL"] = model.api.url if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key if (model.headers) options["headers"] = { ...options["headers"], ...model.headers, } ``` ### Configuration Example After this change, users can configure model-level endpoints: ```json { "provider": { "modelhub-gpt": { "name": "ModelHub GPT", "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "https://default.api.com/v1", "apiKey": "default-key" }, "models": { "gpt-4o": { "id": "gpt-4o", "name": "GPT-4o", "options": { "baseURL": "https://chat.api.com/v1" } }, "gemini-2.0-flash": { "id": "gemini-2.0-flash", "name": "Gemini 2.0 Flash", "options": { "baseURL": "https://gemini.api.com/v1", "apiKey": "gemini-specific-key" } } } } } } ``` ## Impact Analysis ### Backward Compatibility ✅ **Fully backward compatible** - If `model.options` is empty or undefined, behavior is unchanged. - Existing configurations continue to work without modification. - Provider-level `baseURL`/`apiKey` remains the default. ### SDK Caching The current SDK caching mechanism (line 980-982) uses a hash of `{ providerID, npm, options }`: ```typescript const key = Bun.hash.xxHash32(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options })) ``` Since model-level options are now merged into `options` **before** computing the hash, different models with different `baseURL`/`apiKey` will correctly get separate SDK instances. **No additional changes needed.** ### Type Safety The `Model` type already supports `options: z.record(z.string(), z.any())` (line 572), so no schema changes are required. ### Security Considerations - API keys in model-level options follow the same security model as provider-level options. - No new attack vectors introduced. ## Alternative Approaches Considered ### Alternative A: Add dedicated `model.baseURL` and `model.apiKey` fields ```typescript // In Model schema baseURL: z.string().optional(), apiKey: z.string().optional(), ``` **Rejected because:** - Requires schema changes - Less flexible than generic `options` override - Inconsistent with existing `options` pattern ### Alternative B: Use `model.api.url` for baseURL override The current code already has: ```typescript if (!options["baseURL"]) options["baseURL"] = model.api.url ``` **Rejected because:** - `model.api.url` serves a different semantic purpose (API metadata) - Doesn't solve the `apiKey` override requirement - Conflates configuration with metadata ### Alternative C: Handle in plugin custom fetch Let plugins intercept and rewrite URLs in their custom fetch handler. **Rejected because:** - Complex URL parsing and reconstruction - Error-prone (must handle all URL patterns) - Duplicates logic across plugins - Core should provide clean configuration mechanism ## Implementation Checklist - [ ] Add model options merging in `getSDK()` (5 lines) - [ ] Add unit tests for model-level `baseURL` override - [ ] Add unit tests for model-level `apiKey` override - [ ] Add unit tests for mixed provider + model options - [ ] Update documentation ## Test Plan ```typescript describe("getSDK with model-level options", () => { it("should use model baseURL when specified", async () => { // Provider: baseURL = "https://provider.com" // Model: options.baseURL = "https://model.com" // Expected: SDK uses "https://model.com" }) it("should use model apiKey when specified", async () => { // Provider: apiKey = "provider-key" // Model: options.apiKey = "model-key" // Expected: SDK uses "model-key" }) it("should fall back to provider options when model options not specified", async () => { // Provider: baseURL = "https://provider.com", apiKey = "provider-key" // Model: options = {} // Expected: SDK uses provider values }) it("should create separate SDK instances for models with different options", async () => { // Model A: options.baseURL = "https://a.com" // Model B: options.baseURL = "https://b.com" // Expected: Different SDK instances (different cache keys) }) }) ``` ## Conclusion This proposal provides a minimal, backward-compatible change that enables model-level `baseURL` and `apiKey` configuration. The change: 1. **Is minimal** - Only 5 lines of code added 2. **Is safe** - Fully backward compatible, no breaking changes 3. **Is consistent** - Follows existing `options` pattern 4. **Is complete** - Solves both `baseURL` and `apiKey` requirements 5. **Is correct** - SDK caching works correctly with no additional changes
yindo added the discussion label 2026-02-16 18:08:58 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Jan 30, 2026):

This issue might be a duplicate of existing issues. Please check:

  • #10950: [Bug]: Stored OAuth credentials silently override explicit provider config - addresses similar provider-level configuration override concerns
  • #11106: [FEATURE]: Z.ai Coding Plan endpoint override - requests model-specific endpoint override (baseURL) capability
  • #11209: [FEATURE]: Set default model for plan and build separately - requests model-level configuration granularity

The scope of issue #11287 is broader and more systematic (supporting model-level baseURL and apiKey in the options object), while these related issues tackle specific aspects of this same problem. Consider reviewing these for additional context.

Feel free to ignore if this doesn't address your specific case.

@github-actions[bot] commented on GitHub (Jan 30, 2026): This issue might be a duplicate of existing issues. Please check: - #10950: [Bug]: Stored OAuth credentials silently override explicit provider config - addresses similar provider-level configuration override concerns - #11106: [FEATURE]: Z.ai Coding Plan endpoint override - requests model-specific endpoint override (baseURL) capability - #11209: [FEATURE]: Set default model for plan and build separately - requests model-level configuration granularity The scope of issue #11287 is broader and more systematic (supporting model-level baseURL and apiKey in the options object), while these related issues tackle specific aspects of this same problem. Consider reviewing these for additional context. Feel free to ignore if this doesn't address your specific case.
Author
Owner

@legendtkl commented on GitHub (Jan 30, 2026):

hi, @thdxr do you think if this is ok? If yes, I would like to contribute a PR for this.

@legendtkl commented on GitHub (Jan 30, 2026): hi, @thdxr do you think if this is ok? If yes, I would like to contribute a PR for this.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#8036