opencode icon indicating copy to clipboard operation
opencode copied to clipboard

[FEATURE]: multiple auth profiles per provider

Open charles-cooper opened this issue 2 weeks ago • 6 comments

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

this is a reopening of https://github.com/sst/opencode/issues/893. i think it would be helpful to be able to have multiple profiles per provider. they can be switched between in the provider picker or set via command like (e.g. --provider X --profile Y).

https://github.com/sst/opencode/issues/893#issuecomment-3064353293 recommends an opencode.json per directory, but it would be nice to be able to maintain it globally and/or from the command line

charles-cooper avatar Dec 11 '25 17:12 charles-cooper

I'd enjoy this feature, but personally would prefer if I could switch profiles without having to exit/restart opencode... not immediately certain what the best UI for that would be, I'll have to think about that.

ariane-emory avatar Dec 12 '25 06:12 ariane-emory

I'd enjoy this feature, but personally would prefer if I could switch profiles without having to exit/restart opencode... not immediately certain what the best UI for that would be, I'll have to think about that.

yes exactly, this is the improvement over the suggestion of using an opencode.json per directory

charles-cooper avatar Dec 13 '25 00:12 charles-cooper

You can switch profiles without exiting using /connect but you can only have 1 profile at a time.

So yes you can switch between already @ariane-emory but ofc not as nicely as what this feature is suggesting

rekram1-node avatar Dec 13 '25 01:12 rekram1-node

i think what's needed to implement this is basically a picker in the UI and an option to set provider/profile for session on the opencode server

charles-cooper avatar Dec 13 '25 14:12 charles-cooper

Proposed Implementation Plan

I've analyzed the issue with Claude and designed an approach for this feature. Requesting feedback before implementation.

SUMMARY.md:

# Summary: Multiple Auth Profiles per Provider

## Issue Overview

**Issue**: [#5391](https://github.com/sst/opencode/issues/5391) - Multiple auth profiles per provider

**Reopening of**: [#893](https://github.com/sst/opencode/issues/893)

**Problem**: Users who have multiple API keys for the same provider (e.g., two OpenRouter keys for separate projects with separate cost tracking) cannot currently manage them. When running `opencode auth login` multiple times for the same provider, only the latest key is stored, overwriting the previous one.

## Current Architecture

### Auth Storage (`packages/opencode/src/auth/index.ts`)

- Auth credentials are stored in `~/.local/share/opencode/auth.json` (XDG data directory)
- Storage is keyed **only by provider ID**: `Record<string, Auth.Info>`
- Auth types: `oauth`, `api` (API key), `wellknown`
- When `Auth.set(providerID, info)` is called, it overwrites any existing auth for that provider

### Provider System (`packages/opencode/src/provider/provider.ts`)

- Providers are loaded from `models.dev` database and merged with config
- Provider lookup: `providers[providerID]` - single provider per ID
- Model selection uses `provider/model` format (e.g., `anthropic/claude-sonnet-4`)
- SDK instances cached by hash of `{npm, options}` including `apiKey`

### Config System (`packages/opencode/src/config/config.ts`)

- Supports `provider` config with custom options including `apiKey`
- Providers can be defined in `opencode.json` with custom settings
- No concept of "profiles" exists in current config schema

### CLI Auth Commands (`packages/opencode/src/cli/cmd/auth.ts`)

- `auth login`: Select provider, optionally use plugin auth, or enter API key
- `auth logout`: Select provider to remove
- `auth list`: Shows all stored credentials by provider ID
- No support for specifying a profile name during auth operations

### CLI Run Command (`packages/opencode/src/cli/cmd/run.ts`)

- Accepts `--model` flag in format `provider/model`
- No `--profile` flag exists

## Key Files

| File                                         | Purpose                             |
| -------------------------------------------- | ----------------------------------- |
| `packages/opencode/src/auth/index.ts`        | Auth storage (get/set/all/remove)   |
| `packages/opencode/src/provider/provider.ts` | Provider loading, SDK creation      |
| `packages/opencode/src/provider/auth.ts`     | Provider auth methods (OAuth flows) |
| `packages/opencode/src/config/config.ts`     | Config schema and loading           |
| `packages/opencode/src/cli/cmd/auth.ts`      | CLI auth commands                   |
| `packages/opencode/src/cli/cmd/run.ts`       | CLI run command                     |
| `packages/opencode/src/flag/flag.ts`         | Environment variable flags          |

## User Requests

1. Multiple profiles per provider stored globally
2. Profile switching in provider picker (TUI)
3. Command-line specification: `--provider X --profile Y`
4. Ability to maintain profiles globally, not just per-directory config

PLAN.md:

# Implementation Plan: Multiple Auth Profiles per Provider

## Overview

Add support for multiple authentication profiles per provider, allowing users to maintain separate API keys for the same provider (e.g., different OpenRouter keys for different cost centers).

---

## Phase 1: Core Auth Module Changes

### File: `packages/opencode/src/auth/index.ts`

**1.1 Add profile utilities**

```typescript
// Parse composite key into provider and profile
function parseKey(key: string): { providerID: string; profile?: string }

// Build composite key from provider and profile
function buildKey(providerID: string, profile?: string): string

// Validate profile name (alphanumeric, hyphen, underscore)
function validateProfileName(name: string): boolean
```

**1.2 Update `Auth.get()` signature**

```typescript
// Before
export async function get(providerID: string): Promise<Info | undefined>

// After
export async function get(providerID: string, profile?: string): Promise<Info | undefined>
```

**1.3 Update `Auth.set()` signature**

```typescript
// Before
export async function set(key: string, info: Info): Promise<void>

// After
export async function set(providerID: string, info: Info, profile?: string): Promise<void>
```

**1.4 Update `Auth.remove()` signature**

```typescript
// Before
export async function remove(key: string): Promise<void>

// After
export async function remove(providerID: string, profile?: string): Promise<void>
```

**1.5 Add new functions**

```typescript
// List all profiles for a provider
export async function profiles(providerID: string): Promise<Array<{ profile?: string; info: Info }>>

// Check if default profile exists for provider
export async function hasDefault(providerID: string): Promise<boolean>

// Swap default with a named profile
export async function setDefault(providerID: string, profile: string): Promise<void>
```

---

## Phase 2: Provider Module Changes

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

**2.1 Update `parseModel()` function**

```typescript
// Before: "openrouter/claude-sonnet-4" -> { providerID, modelID }
// After:  "openrouter:work/claude-sonnet-4" -> { providerID, profile?, modelID }

export function parseModel(model: string): {
  providerID: string
  profile?: string
  modelID: string
}
```

**2.2 Update auth loading in `state()`**

Around line 624, where `Auth.all()` is iterated:

```typescript
// Current: iterates provider IDs directly
// New: parse composite keys, group by provider, resolve which profile to use
```

**2.3 Update `getSDK()` to accept profile**

The SDK cache key already includes `apiKey` in options hash, so different profiles will naturally get different SDK instances. Just need to ensure the correct key is passed through.

---

## Phase 3: Config Schema Changes

### File: `packages/opencode/src/config/config.ts`

**3.1 Extend Provider schema**

```typescript
export const Provider = ModelsDev.Provider.partial().extend({
  // ... existing fields
  profile: z.string().optional().describe("Default auth profile to use for this provider"),
})
```

---

## Phase 4: CLI Auth Commands

### File: `packages/opencode/src/cli/cmd/auth.ts`

**4.1 Update `AuthLoginCommand`**

- After provider selection, check if default profile exists
- If exists, prompt for profile name (with validation)
- If not exists, create as default (no profile suffix)
- Update `Auth.set()` calls to include profile parameter

**4.2 Update `AuthListCommand`**

- Group credentials by provider
- Show profile names (or "default" for no-suffix keys)
- Format:
  ```
  OpenRouter
    default                 api
    work                    api
  Anthropic
    default                 oauth
  ```

**4.3 Update `AuthLogoutCommand`**

- Show all credentials with profile names in selection
- Update `Auth.remove()` call to use parsed provider/profile

**4.4 Add `AuthSetDefaultCommand`**

- Select provider (only show providers with multiple profiles)
- Select profile to promote to default
- Call `Auth.setDefault(providerID, profile)`

---

## Phase 5: Provider Auth Module

### File: `packages/opencode/src/provider/auth.ts`

**5.1 Update `api()` function**

Add profile parameter:

```typescript
export const api = fn(
  z.object({
    providerID: z.string(),
    key: z.string(),
    profile: z.string().optional(),
  }),
  async (input) => {
    await Auth.set(input.providerID, { type: "api", key: input.key }, input.profile)
  },
)
```

**5.2 Update OAuth callback handling**

Ensure profile is passed through OAuth flows when saving credentials.

---

## Phase 6: CLI Run Command

### File: `packages/opencode/src/cli/cmd/run.ts`

**6.1 Update model parsing**

The `--model` flag already goes through `Provider.parseModel()`, so after Phase 2.1, profile support will work automatically.

---

## Phase 7: TUI Changes (if applicable)

### Files in `packages/opencode/src/cli/cmd/tui/`

**7.1 Provider/Model picker**

- Group providers with multiple profiles
- Show nested selection: Provider → Profile → Model
- Or flat list with "Provider (profile)" format

---

## Phase 8: Error Handling

### New error types needed:

```typescript
// In auth/index.ts
export const ProfileNotFoundError = NamedError.create(
  "AuthProfileNotFoundError",
  z.object({
    providerID: z.string(),
    profile: z.string(),
  }),
)

export const InvalidProfileNameError = NamedError.create(
  "AuthInvalidProfileNameError",
  z.object({
    profile: z.string(),
  }),
)
```

---

## Implementation Notes

1. **Composite key format**: Keys in `auth.json` use `providerID` for default, `providerID:profileName` for named profiles. The `:` character is the delimiter.

2. **Profile name validation**: `/^[a-zA-Z0-9_-]+$/` - alphanumeric plus hyphen and underscore.

3. **Backward compatibility**: All existing auth.json files work unchanged. Existing code calling `Auth.get(providerID)` without profile gets the default.

4. **Profile resolution order**:
   - Explicit in model string (`provider:profile/model`)
   - Config setting (`provider.X.profile`)
   - Default profile (no suffix in auth.json)

5. **Non-existent profile**: Error immediately, no fallback to default.

---

## Testing Considerations

1. Migration: existing auth.json files should work without changes
2. Multiple profiles for same provider stored and retrieved correctly
3. Profile in model string parsed correctly
4. Config profile override works
5. Error on non-existent profile
6. `set-default` swap works correctly
7. CLI commands handle profiles in all flows

---

## Order of Implementation

1. Phase 1 (Auth module) - foundation
2. Phase 3 (Config schema) - small change, needed for resolution
3. Phase 2 (Provider module) - uses Auth + Config
4. Phase 4 (CLI auth commands) - user-facing
5. Phase 5 (Provider auth) - OAuth flows
6. Phase 6 (CLI run) - should mostly work after Phase 2
7. Phase 7 (TUI) - if needed
8. Phase 8 (Errors) - can be added incrementally

Looking for feedback on this approach before starting implementation. Any concerns or alternative suggestions?

charles-cooper avatar Dec 13 '25 16:12 charles-cooper

For a workaround in the meantime for the same provider (I have two subscriptions for Anthropic). I use a different directory for the auth values, which means no changes are needed to opencode.json:

alias opencode-work='XDG_DATA_HOME=~/.local/share/opencode-work opencode'

Still not perfect, but at least I don't have to re-auth just to switch to the same provider.

grenaad avatar Dec 13 '25 20:12 grenaad