Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ and a `type` (well-known identifier). Manage them with `netclaw provider`:
|------|------|-------|
| `ollama` | Endpoint only | `--endpoint http://host:11434` |
| `openai` | API key **or** OAuth (ChatGPT sub) | Codex backend for OAuth path |
| `openai-compatible` | Endpoint; optional API key | Generic OpenAI-shape proxies, llama.cpp, vLLM. Also DwarfStar (ds4): `--endpoint http://127.0.0.1:8000`, run `ds4-server` separately, model ids `deepseek-v4-flash` / `deepseek-v4-pro`, context window auto-detected |
| `openai-compatible` | Endpoint; optional API key (Bearer) | Generic OpenAI-shape proxies, llama.cpp, vLLM. Also DwarfStar (ds4): `--endpoint http://127.0.0.1:8000`, run `ds4-server` separately, model ids `deepseek-v4-flash` / `deepseek-v4-pro`, context window auto-detected. For gated endpoints (LiteLLM, intranet gateways), add `--api-key <key>`; the key is stored in `secrets.json` and sent as `Authorization: Bearer`. `netclaw init` and the `netclaw provider` TUI offer the same choice ("No auth (local endpoint)" vs "API Key"). An entry that declares `AuthMethod: ApiKey` without a stored key is reported by `netclaw doctor`. |
| `anthropic` | API key | `sk-ant-...` |
| `openrouter` | API key | `sk-or-...` |
| `github-copilot` | OAuth device flow only | Requires active Copilot subscription on the GitHub account |
| `veniceai` | API key | OpenAI-compatible at `https://api.venice.ai/api/v1`. Suppresses Venice's prepended system prompt by default; opt in via `VendorOptions.IncludeVeniceSystemPrompt = true` |
| `deepseek` | API key | DeepSeek hosted API at `https://api.deepseek.com/v1`. Current model ids: `deepseek-v4-flash` and `deepseek-v4-pro` |
| `zai` | API key | Z.ai GLM Coding Plan at `https://api.z.ai/api/coding/paas/v4`. Current models: `glm-5.3`, `glm-5-turbo`, `glm-4.7`; requests for `glm-5.2`/`glm-5.1` are server-routed to `glm-5.3`. For the pay-as-you-go platform, set `--endpoint https://api.z.ai/api/paas/v4` |

Provider-specific behavior toggles belong under
`Providers.<name>.VendorOptions`. Netclaw keeps that bag opaque at the core
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-openai-compatible-auth/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-19
111 changes: 111 additions & 0 deletions openspec/changes/add-openai-compatible-auth/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## Context

The `openai-compatible` provider type (`OpenAiCompatibleDescriptor`,
TypeKey `openai-compatible`) targets self-hosted OpenAI-shaped backends. Its
transport already supports auth: `OpenAiCompatibleChatClient.BuildRequest`
sets `Authorization: Bearer` when `ApiKey` is present, and the probe, models
client, and capability resolver do the same. The CLI
(`netclaw provider add <name> openai-compatible --endpoint <url> --api-key <key>`)
writes the key to encrypted secrets today.

The gap is the auth declaration. `Auth` is `EndpointOnlyAuth`
(`SupportedAuthMethods = [AuthMethod.None]`), and the TUI drives off that:
- The wizard skips the auth sub-step when the method set is `[None]`.
- `OAuthFlowViews.BuildAuthMethodLabels` filters out `AuthMethod.None`.
- `ProviderStepView.BuildCredentialInput` and
`ProviderManagerPage.BuildCredentialsView` switch on the concrete auth type:
`EndpointOnlyAuth` shows endpoint only; every other shape shows API-key
only. No shape today means "endpoint plus optional key".

So an operator cannot reach the already-working auth path from any
interactive surface, and the declared contract misdescribes the runtime.

## Goals / Non-Goals

**Goals:**

- The `openai-compatible` auth method set is `[None, ApiKey]`.
- The wizard and the provider manager offer both auth choices with an
explicit "No auth" label for `None`.
- Credential screens show endpoint input plus an optional API-key input for
this shape.
- An entered key is probed with Bearer, persisted to encrypted secrets, and
recorded as `AuthMethod.ApiKey`; an empty key behaves exactly as today.
- Existing no-auth configurations are untouched in behavior.

**Non-Goals:**

- No transport change — the wire paths already send Bearer when a key exists.
- No non-Bearer header schemes (`api-key`, `x-api-key`, Azure-style).
- No per-instance display names, no new type keys, no OAuth for this shape.
- No config schema change (`Providers` is schema-open).

## Decisions

### D1: One new auth shape, not an extended `EndpointOnlyAuth`

Add `EndpointOrApiKeyAuth : IProviderAuth` with
`SupportedAuthMethods = [AuthMethod.None, AuthMethod.ApiKey]`.

Rationale: the TUI switches on concrete auth types
(`IProviderAuth` doc comment states this contract). Changing
`EndpointOnlyAuth` to carry two methods would flip every existing
endpoint-only consumer — including Ollama — into new UI paths. A distinct
shape confines the change to `openai-compatible`.
_Alternative rejected:_ making `EndpointOnlyAuth.SupportApiKey` configurable —
same concrete-type switch, but with hidden state that the TUI must also
switch on; two axes where one type each suffices.

### D2: Method order — `None` first

`SupportedAuthMethods = [None, ApiKey]` keeps "No auth" as the default
selection wherever the picker defaults to index 0. Local backends stay the
common case; auth is opt-in per instance.

### D3: `None` gets an explicit auth-picker label

`BuildAuthMethodLabels` currently drops `AuthMethod.None` because no
multi-method provider offered it. For this shape the wizard shows an auth
picker with two labeled choices; the label for `None` is "No auth (local
endpoint)". Selection drives which credential fields appear and which
`AuthMethod` is persisted. This also preserves the existing skip behavior:
single-`None` providers (Ollama) still bypass the picker entirely.

### D4: Optional key input, not two sequential screens

The credential screen for this shape shows the endpoint input first (Enter
advances), then the API-key input where an empty submit means "no key".
Empty submit stores no secret and persists `AuthMethod.None`. A non-empty
key persists `AuthMethod.ApiKey` and writes the secret.

Rationale: one screen with a clear skip matches the "optional" contract and
avoids a modal question before every field.

### D5: No new validation gate in the daemon

Startup tri-state validation stays as is: a provider entry with
`AuthMethod.ApiKey` and a missing key must fail visibly through the existing
per-descriptor credential check (`ChatClientDoctorCheck.MissingCredentialMessage`),
not a new validator. The descriptor's `Auth` shape already declares that
`ApiKey` is one supported method, and the existing doctor logic handles
method/credential mismatch.

## Risks / Trade-offs

- **Concrete-type switches in TUI** — two views branch on the auth type today;
this adds a third branch in each. Accepted: the `IProviderAuth` contract
documents the switch. A generic field-driven auth model would be a larger
refactor with no additional behavior.
- **Smoke tape churn** — the wizard and provider-manager tapes drive the
provider flow by list index; a new auth sub-step changes the keystroke
sequence. Mitigation: update `init-wizard.tape` and `provider-add.tape`
in the same PR and run the light smoke suite.
- **Wrong method/credential combinations via hand-edited config** (for
example `AuthMethod: ApiKey` with no key) — pre-existing behavior; doctor
reports it. Not made worse by this change; covered by a fake-failure test
at the TUI save boundary.

## Open Questions

None — design decisions confirmed with the operator during planning:
extend `openai-compatible` (no new type key), optional key, Bearer only.
68 changes: 68 additions & 0 deletions openspec/changes/add-openai-compatible-auth/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## Why

The `openai-compatible` provider type covers self-hosted OpenAI-shaped
backends such as llama.cpp, vLLM, Lemonade, and DwarfStar (ds4). The runtime
transport already sends `Authorization: Bearer` when an API key is present —
in the probe, the chat client, the models client, and the capability resolver.
But the descriptor declares `EndpointOnlyAuth`, so the auth method set is
`[None]` only. The init wizard and the provider manager TUI therefore never
offer a key input, and operators cannot configure an authenticated
OpenAI-compatible backend through any interactive surface.

Many deployments need that key: gated intranet gateways, LiteLLM proxies,
and hosted OpenAI-compatible APIs all require Bearer auth.

## What Changes

- Add one `IProviderAuth` shape: `EndpointOrApiKeyAuth`, supporting
`[AuthMethod.None, AuthMethod.ApiKey]`.
- Change `OpenAiCompatibleDescriptor.Auth` from `EndpointOnlyAuth` to the new
shape. No runtime transport change — all wire paths already send Bearer
when a key exists and send no header when it does not.
- Init wizard: show the auth-method picker for this provider with an explicit
"No auth" choice; show endpoint input plus an optional API-key input; probe
and persist the key through the existing encrypted secrets path with
`AuthMethod.ApiKey`.
- Provider manager TUI (`netclaw provider`): the same two additions.
- Update the `netclaw-operations` system skill provider reference and bump its
version.
- Update smoke tapes that drive the wizard and provider-manager flows.
- No schema change: the `Providers` schema section is open. No new config
knob. Not breaking — existing `AuthMethod: None` entries keep their behavior.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `netclaw-model-providers`: the `openai-compatible` provider gains optional
API-key authentication. The auth method set becomes `[None, ApiKey]`. The
key is optional; an absent key sends no auth header. Interactive surfaces
(init wizard, provider manager) offer both choices and persist the key
through the encrypted secrets path.

## Impact

- **Code:** `src/Netclaw.Providers/IProviderAuth.cs` (new shape),
`src/Netclaw.Providers/SelfHosted/OpenAiCompatibleDescriptor.cs` (auth
declaration), `src/Netclaw.Cli/Tui/Wizard/Steps/ProviderStepView.cs` and
`ProviderStepViewModel.cs`, `src/Netclaw.Cli/Tui/OAuthFlowViews.cs` (auth
labels include `None`), `src/Netclaw.Cli/Tui/ProviderManagerViewModel.cs`
and `ProviderManagerPage.cs`.
- **Skill:** `feeds/skills/.system/files/netclaw-operations/references/providers.md`
with a `metadata.version` bump in the skill frontmatter.
- **Tests:** unit tests for the auth shape and the wizard/manager state
transitions; a fake-failure test proving a selected `ApiKey` method with an
empty key blocks save;.
- **No change:** transport code (`OpenAiCompatibleChatClient`,
`OpenAiCompatibleModelsClient`, `OpenAiCompatibleCapabilityResolver`),
CLI `provider add` surface (already accepts `--api-key`), config schema,
persistence, secrets format.
- **Traceability:** multi-provider support requirement in
`openspec/specs/netclaw-model-providers/spec.md`.
- **Out of scope:** non-Bearer header schemes (`api-key`, `x-api-key`),
per-instance display names, new provider type keys, OAuth for
OpenAI-compatible endpoints.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## MODIFIED Requirements

### Requirement: Multi-provider support

The system SHALL support selecting one provider profile from a supported set.
Supported provider type keys SHALL include `ollama`, `openai-compatible`,
`openrouter`, `openai`, `anthropic`, `github-copilot`, and `veniceai`.
All provider interactions SHALL use the Microsoft.Extensions.AI `IChatClient`
abstraction layer, ensuring provider-agnostic model access throughout the
application.

Provider model discovery SHALL extract modality metadata where the provider
API supports it. `DiscoveredModel` records SHALL include `InputModalities`
and `OutputModalities` fields populated from provider responses.

The `openai-compatible` provider SHALL support both no authentication and
API-key authentication. The API key SHALL be optional. When an API key is
configured, all OpenAI-compatible requests (chat completion, model
discovery, capability probing) SHALL send it as `Authorization: Bearer`.
When no API key is configured, requests SHALL send no authentication header.

#### Scenario: Switch provider

- **GIVEN** OpenRouter is configured
- **WHEN** operator selects Anthropic, OpenAI, Ollama, OpenAI-compatible,
OpenRouter, GitHub Copilot, or Venice.ai profile
- **THEN** runtime uses selected provider through the `IChatClient` interface
after validation

#### Scenario: Provider accessed through MEAI abstraction

- **GIVEN** a provider profile is configured
- **WHEN** the session actor sends a chat completion request
- **THEN** the request is routed through the `IChatClient` abstraction
- **AND** no provider-specific types leak into session or actor code

#### Scenario: Ollama discovery includes modality

- **GIVEN** an Ollama provider is configured
- **WHEN** model discovery runs via `ProviderProbe`
- **THEN** the returned `DiscoveredModel` records SHALL include
`InputModalities` and `OutputModalities` populated from `/api/show`
capability data

#### Scenario: OpenRouter discovery includes modality

- **GIVEN** an OpenRouter provider is configured
- **WHEN** model discovery runs via `ProviderProbe`
- **THEN** the returned `DiscoveredModel` records SHALL include
`InputModalities` and `OutputModalities` populated from
`architecture.input_modalities` and `architecture.output_modalities`

#### Scenario: OpenAI-compatible discovery includes backend context metadata

- **GIVEN** an OpenAI-compatible provider is configured
- **WHEN** model discovery runs via `ProviderProbe`
- **THEN** the returned `DiscoveredModel` records SHALL include context-window
metadata when the backend exposes a known field shape, including vLLM
`max_model_len`, DwarfStar/ds4 `context_length` or
`top_provider.context_length`, and llama.cpp `meta.n_ctx` or
`meta.n_ctx_train`

#### Scenario: Add an OpenAI-compatible provider with an API key

- **WHEN** the operator adds an `openai-compatible` provider and supplies an
API key through an interactive surface
- **THEN** Netclaw stores `AuthMethod: ApiKey` in the provider entry
- **AND** Netclaw stores the key through the encrypted secrets path
- **AND** chat, discovery, and probe requests send the key as
`Authorization: Bearer`

#### Scenario: Add an OpenAI-compatible provider without an API key

- **WHEN** the operator adds an `openai-compatible` provider and supplies no
API key
- **THEN** Netclaw stores `AuthMethod: None` and no provider secret
- **AND** chat, discovery, and probe requests send no authentication header

#### Scenario: Existing no-auth OpenAI-compatible configuration

- **GIVEN** an `openai-compatible` provider entry configured before this
change with no API key
- **WHEN** Netclaw loads the configuration after upgrade
- **THEN** the entry behaves exactly as before the upgrade

#### Scenario: API-key auth declared without a stored key

- **GIVEN** an `openai-compatible` provider entry declares
`AuthMethod: ApiKey` and no key is stored in secrets
- **WHEN** configuration diagnostics run
- **THEN** the failure is reported with provider-specific credential guidance
- **AND** Netclaw does not silently fall back to no-auth requests
85 changes: 85 additions & 0 deletions openspec/changes/add-openai-compatible-auth/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
## 1. Auth contract

- [x] 1.1 Add `EndpointOrApiKeyAuth : IProviderAuth` in
`src/Netclaw.Providers/IProviderAuth.cs` with
`SupportedAuthMethods = [AuthMethod.None, AuthMethod.ApiKey]`.
- [x] 1.2 Change `OpenAiCompatibleDescriptor.Auth` from `EndpointOnlyAuth`
to `EndpointOrApiKeyAuth`.
- [x] 1.3 Confirmed no transport change is needed: chat client
(`OpenAiCompatibleChatClient.BuildRequest`), models client, capability
resolver, and descriptor probe send Bearer when a key exists and no header
when it does not. Untouched by this change.

## 2. Init wizard

- [x] 2.1 `OAuthFlowViews.BuildAuthMethodLabels`: `AuthMethod.None` renders
as "No auth (local endpoint)" and is included only for multi-method
providers; single-`None` providers (Ollama) still bypass the picker.
`ParseAuthMethodLabel` round-trips the new label.
- [x] 2.2 `ProviderStepView.BuildCredentialInput`: `EndpointOrApiKeyAuth`
branch — "No auth" shows endpoint input (sub-step 2 → probe); "API Key"
shows endpoint input then a new sub-step 10 for the key input. Back
navigation from 10 returns to 2.
- [x] 2.3 `ProviderStepViewModel`: `BuildProbeEntry` already carries
`ApiKey` when set (no change); `ContributeConfig`/`BuildProviderEntry`
now default the endpoint from descriptors of shape `EndpointOnlyAuth or
EndpointOrApiKeyAuth`; `WriteProviderCredentials` persists the selected
method and encrypted key via the existing `ProviderCredentialWriter`.
- [x] 2.4 `ChatClientDoctorCheck.MissingCredentialMessage`: an entry that
declares `AuthMethod: ApiKey` with no stored key now fails with guidance
(previously any provider supporting `None` skipped all credential checks).

## 3. Provider manager TUI

- [x] 3.1 `AdvanceAfterName` already routes multi-method providers to the
auth picker (no change needed); verified by test.
- [x] 3.2 `BuildAddAuthView` renders both labels via the shared
`BuildAuthMethodLabels` (covered by 2.1).
- [x] 3.3 New `AddCredentialsEndpoint` state + `BuildCredentialsEndpointView`
(endpoint stage before key stage for the ApiKey path);
`BuildCredentialsView` handles `EndpointOrApiKeyAuth` for both methods;
new `FixApiKey` state + `BuildFixApiKeyView` for repairing a key'd entry
(endpoint stage first, then key stage, via `SubmitFixEndpoint`).
- [x] 3.4 `WriteProviderConfig` persists via `ProviderCredentialWriter` with
the selected `NewAuthMethod` (no change needed); `SubmitFixCredentials`
key-required guard corrected to require a key only when the type is
key-only OR the entry declares `AuthMethod.ApiKey` (fixes a latent
regression where a no-auth openai-compatible entry would have demanded a
key).

## 4. Tests

- [x] 4.1 `OpenAiCompatibleAuthTests.OpenAiCompatible_Auth_SupportsNoneAndApiKeyInOrder`.
- [x] 4.2 Manager VM transitions: None → `AddCredentials`; ApiKey →
`AddCredentialsEndpoint` → key stage; empty-key ApiKey submit blocks.
- [x] 4.3 Fake-failure gates: `SubmitCredentials_...EmptyKey_BlocksBeforeProbe`
(no probe, no config write) and
`SubmitFixCredentials_...ApiKeyEntryWithEmptyKey_Blocks`.
- [x] 4.4 Wizard equivalents: probe entry carries key / no key;
`ContributeConfig` both methods; `WriteProviderCredentials` both methods
(encrypted secret asserted via `ENC:` prefix).
- [x] 4.5 Headless typed-key end-to-end:
`ManagerAddFlow_OpenAiCompatibleApiKey_TypedKeyEndToEnd` drives the real
page through type list → name → auth picker → endpoint → key → AddComplete
and asserts the persisted config. No `Thread.Sleep`/`Task.Delay` in
orchestration (polling via `Task.Yield` + cancellation).
- [x] 4.6 Doctor:
`ReturnsError_WhenOpenAiCompatibleDeclaresApiKeyWithoutStoredKey`,
`ReturnsPass_WhenOpenAiCompatibleUsesNoAuth`.

## 5. Operator guidance

- [x] 5.1 `feeds/skills/.system/files/netclaw-operations/references/providers.md`:
`openai-compatible` row documents Bearer, `--api-key`, the TUI choice, and
the doctor report for a declared-ApiKey-without-key entry.
- [x] 5.2 Skill version bumped 2.56.0 → 2.57.0.

## 7. Quality gates

- [x] 7.1 `dotnet build` clean (0 warnings). Netclaw.Cli.Tests 1389/1391
(2 pre-existing environment skips); Netclaw.Daemon.Tests 1047/1047;
Netclaw.Configuration.Tests 604/604.
- [x] 7.2 `dotnet slopwatch analyze` — one pre-existing SW004 warning in
`PowerShellHostProbeTests.cs` (outside this diff, documented in a prior
change). No new violations.
- [x] 7.3 `./scripts/Add-FileHeaders.ps1 -Verify` — all files have headers.
2 changes: 2 additions & 0 deletions openspec/changes/add-zai-provider/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-10
Loading