diff --git a/.claude/commands/openspec/apply.md b/.claude/commands/openspec/apply.md new file mode 100644 index 000000000000..a36fd964efda --- /dev/null +++ b/.claude/commands/openspec/apply.md @@ -0,0 +1,23 @@ +--- +name: OpenSpec: Apply +description: Implement an approved OpenSpec change and keep tasks in sync. +category: OpenSpec +tags: [openspec, apply] +--- + +**Guardrails** +- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required. +- Keep changes tightly scoped to the requested outcome. +- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications. + +**Steps** +Track these steps as TODOs and complete them one by one. +1. Read `changes//proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria. +2. Work through tasks sequentially, keeping edits minimal and focused on the requested change. +3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished. +4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality. +5. Reference `openspec list` or `openspec show ` when additional context is required. + +**Reference** +- Use `openspec show --json --deltas-only` if you need additional context from the proposal while implementing. + diff --git a/.claude/commands/openspec/archive.md b/.claude/commands/openspec/archive.md new file mode 100644 index 000000000000..dbc769586f15 --- /dev/null +++ b/.claude/commands/openspec/archive.md @@ -0,0 +1,27 @@ +--- +name: OpenSpec: Archive +description: Archive a deployed OpenSpec change and update specs. +category: OpenSpec +tags: [openspec, archive] +--- + +**Guardrails** +- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required. +- Keep changes tightly scoped to the requested outcome. +- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications. + +**Steps** +1. Determine the change ID to archive: + - If this prompt already includes a specific change ID (for example inside a `` block populated by slash-command arguments), use that value after trimming whitespace. + - If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends. + - Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding. + - If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet. +2. Validate the change ID by running `openspec list` (or `openspec show `) and stop if the change is missing, already archived, or otherwise not ready to archive. +3. Run `openspec archive --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work). +4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`. +5. Validate with `openspec validate --strict` and inspect with `openspec show ` if anything looks off. + +**Reference** +- Use `openspec list` to confirm change IDs before archiving. +- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off. + diff --git a/.claude/commands/openspec/proposal.md b/.claude/commands/openspec/proposal.md new file mode 100644 index 000000000000..cbb75ce24c91 --- /dev/null +++ b/.claude/commands/openspec/proposal.md @@ -0,0 +1,28 @@ +--- +name: OpenSpec: Proposal +description: Scaffold a new OpenSpec change and validate strictly. +category: OpenSpec +tags: [openspec, change] +--- + +**Guardrails** +- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required. +- Keep changes tightly scoped to the requested outcome. +- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications. +- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files. +- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval. + +**Steps** +1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification. +2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes//`. +3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing. +4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs. +5. Draft spec deltas in `changes//specs//spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant. +6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work. +7. Validate with `openspec validate --strict` and resolve every issue before sharing the proposal. + +**Reference** +- Use `openspec show --json --deltas-only` or `openspec show --type spec` to inspect details when validation fails. +- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones. +- Explore the codebase with `rg `, `ls`, or direct file reads so proposals align with current implementation realities. + diff --git a/packages/opencode/openspec/changes/fix-provider-baseurl-validation/proposal.md b/packages/opencode/openspec/changes/fix-provider-baseurl-validation/proposal.md new file mode 100644 index 000000000000..ef46335bc57a --- /dev/null +++ b/packages/opencode/openspec/changes/fix-provider-baseurl-validation/proposal.md @@ -0,0 +1,142 @@ +# Change: Validate provider baseURL format before passing to SDK + +## Why + +When users misconfigure their `opencode.json` with an invalid `api` field (e.g., `"api": "anthropic"` instead of a URL), or when invalid values slip through the options merge chain, the SDK receives a malformed `baseURL`. The `@ai-sdk/*` SDKs use nullish coalescing (`??`) for defaults, which doesn't catch empty strings or non-URL values—causing cryptic `ERR_INVALID_URL` errors from `fetch()`. + +## What Changes + +### Phase 1: Runtime Validation (Complete) + +- Add URL format validation before passing `baseURL` to provider SDKs +- Only set `baseURL` if it's a valid URL (starts with `http://` or `https://`) +- Delete invalid `baseURL` values so SDKs fall back to their hardcoded defaults + +### Phase 2: Config-Time Validation (Planned) + +- Add validation at config load time to catch invalid values early +- Hybrid approach: validate against models.dev data when available, fall back to URL format check +- Provide clear error messages pointing users to the exact config issue + +## Impact + +- Affected code: + - Phase 1: `packages/opencode/src/provider/provider.ts` + - Phase 2: `packages/opencode/src/config/config.ts` +- Risk: Low - only filters out invalid URLs, cannot break valid configurations +- User benefit: Clear failure mode instead of cryptic fetch errors + +## Root Cause Analysis + +### The Problem + +The `@ai-sdk/anthropic` SDK (and others) construct their baseURL like this: + +```javascript +const baseURL = withoutTrailingSlash(loadOptionalSetting({ + settingValue: options.baseURL, + environmentVariableName: "ANTHROPIC_BASE_URL" +})) ?? "https://api.anthropic.com/v1"; +``` + +The `??` operator only catches `null`/`undefined`. If `baseURL` is: +- Empty string `""` +- Invalid value like `"anthropic"` + +...it bypasses the default and causes `fetch()` to fail with `ERR_INVALID_URL`. + +### How Invalid Values Reach the SDK + +1. **User config misconfiguration**: `"api": "anthropic"` instead of a URL +2. **models.dev API**: Returns `"api": null` for some providers +3. **Options merge chain**: Various sources merged without validation + +## Phase 1: Runtime Fix (Complete) + +```typescript +const isValidUrl = (url: string | undefined | null): url is string => { + return typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://")) +} +if (!isValidUrl(options["baseURL"])) { + delete options["baseURL"] + if (isValidUrl(model.api.url)) options["baseURL"] = model.api.url +} +``` + +This ensures only valid URLs reach the SDK; otherwise, the SDK uses its default. + +## Phase 2: Config-Time Validation Design + +### Validation Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ Config Validation Flow │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. Load config file (JSONC parse) │ +│ │ │ +│ ▼ │ +│ 2. Zod schema validation (Info.safeParse) │ +│ │ │ +│ ▼ │ +│ 3. Post-validation hook (NEW) │ +│ ┌───────────────┴───────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ Try models.dev cache Cache unavailable? │ +│ │ │ │ +│ ▼ ▼ │ +│ Validate against URL format check │ +│ known providers (http/https prefix) │ +│ │ │ │ +│ └───────────────┬───────────────┘ │ +│ ▼ │ +│ 4. Return validated config or throw error │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Hybrid Validation Strategy + +**Primary: Validate against models.dev data** + +When models.dev cache is available (`~/.cache/opencode/models.json`): +- Check if provider `api` field matches expected format for known providers +- Known providers with dedicated SDKs (anthropic, openai, groq) expect `null` or valid URL +- Other providers from models.dev show what valid `api` URLs look like + +**Fallback: URL format validation** + +When models.dev cache is unavailable: +- If `api` field is set, validate it starts with `http://` or `https://` +- Reject obviously invalid values like `"anthropic"`, `""`, or random strings + +### What models.dev Returns + +| Provider Type | `api` Field Value | +|---------------|-------------------| +| SDK-based (anthropic, openai) | `null` or not set | +| URL-based (moonshot, nvidia) | `"https://api.example.com/v1"` | + +### Implementation Location + +Post-validation in `Config.loadFile()` after `Info.safeParse()`: + +```typescript +// After line 963: const parsed = Info.safeParse(data) +if (parsed.success && parsed.data.provider) { + await validateProviderConfig(parsed.data.provider) +} +``` + +### Error Messages + +``` +Config validation error at opencode.json: + provider.anthropic.api: Invalid value "anthropic" + Expected: Valid URL (https://...) or omit to use SDK default + + Hint: Remove the "api" field to use the default Anthropic endpoint, + or provide a valid proxy URL like "https://my-proxy.com/v1" +``` diff --git a/packages/opencode/openspec/changes/fix-provider-baseurl-validation/specs/provider/spec.md b/packages/opencode/openspec/changes/fix-provider-baseurl-validation/specs/provider/spec.md new file mode 100644 index 000000000000..86002d29549f --- /dev/null +++ b/packages/opencode/openspec/changes/fix-provider-baseurl-validation/specs/provider/spec.md @@ -0,0 +1,111 @@ +## ADDED Requirements + +### Requirement: Provider baseURL Runtime Validation (Phase 1) + +The system SHALL validate that `baseURL` values passed to provider SDKs are valid URLs (starting with `http://` or `https://`). Invalid or empty values SHALL be removed so the SDK can use its default endpoint. + +#### Scenario: User misconfigures api field with provider name + +- **WHEN** user sets `"api": "anthropic"` in config instead of a URL +- **THEN** the invalid value is filtered out +- **AND** the SDK uses its default baseURL (`https://api.anthropic.com/v1`) + +#### Scenario: baseURL is empty string + +- **WHEN** baseURL is set to empty string `""` +- **THEN** the empty value is deleted from options +- **AND** the SDK uses its default baseURL + +#### Scenario: baseURL is null from models.dev + +- **WHEN** models.dev API returns `"api": null` for a provider +- **THEN** the null value does not override SDK defaults +- **AND** the SDK uses its hardcoded default baseURL + +#### Scenario: Valid baseURL is preserved + +- **WHEN** baseURL is a valid URL like `"https://custom-proxy.example.com/v1"` +- **THEN** the URL is passed to the SDK unchanged +- **AND** the SDK uses the custom baseURL + +--- + +### Requirement: Provider Config Validation at Load Time (Phase 2) + +The system SHALL validate provider configuration fields when loading `opencode.json`, using a hybrid validation strategy that validates against models.dev data when available and falls back to URL format validation otherwise. + +#### Scenario: Validate against models.dev data (primary) + +- **GIVEN** models.dev cache is available at `~/.cache/opencode/models.json` +- **WHEN** config contains a provider with an `api` field +- **THEN** the system validates the `api` value against the expected format for that provider +- **AND** rejects values that don't match (e.g., `"anthropic"` instead of a URL) + +#### Scenario: Fallback to URL format validation + +- **GIVEN** models.dev cache is NOT available +- **WHEN** config contains a provider with an `api` field +- **THEN** the system validates that `api` starts with `http://` or `https://` +- **AND** rejects obviously invalid values like `"anthropic"` or `""` + +#### Scenario: Clear error message on validation failure + +- **WHEN** config validation fails due to invalid `api` value +- **THEN** the error message SHALL include: + - The exact field path (e.g., `provider.anthropic.api`) + - The invalid value that was provided + - A hint explaining valid options (URL or omit field) + +#### Scenario: Valid config passes validation + +- **WHEN** config contains valid provider configuration +- **AND** `api` field is either omitted, null, or a valid URL +- **THEN** validation passes without error +- **AND** config is loaded normally + +--- + +## Validation Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ Config Validation Flow │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. Load config file (JSONC parse) │ +│ │ │ +│ ▼ │ +│ 2. Zod schema validation (Info.safeParse) │ +│ │ │ +│ ▼ │ +│ 3. Post-validation hook │ +│ ┌───────────────┴───────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ Try models.dev cache Cache unavailable? │ +│ │ │ │ +│ ▼ ▼ │ +│ Validate against URL format check │ +│ known providers (http/https prefix) │ +│ │ │ │ +│ └───────────────┬───────────────┘ │ +│ ▼ │ +│ 4. Return validated config or throw error │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Defense in Depth + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Layer 1 │ │ Layer 2 │ │ Layer 3 │ +│ Config │────▶│ Runtime │────▶│ SDK │ +│ Validation │ │ Validation │ │ Defaults │ +│ (Phase 2) │ │ (Phase 1) │ │ (Fallback) │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + ▼ ▼ ▼ + Catch at Catch at Last + load time runtime resort +``` diff --git a/packages/opencode/openspec/changes/fix-provider-baseurl-validation/tasks.md b/packages/opencode/openspec/changes/fix-provider-baseurl-validation/tasks.md new file mode 100644 index 000000000000..b6e27d73aff1 --- /dev/null +++ b/packages/opencode/openspec/changes/fix-provider-baseurl-validation/tasks.md @@ -0,0 +1,100 @@ +# Tasks + +## Phase 1: Runtime Validation + +### 1.1 Implementation + +- [x] 1.1.1 Add `isValidUrl` helper function to validate URL format +- [x] 1.1.2 Replace falsy check with URL format validation +- [x] 1.1.3 Delete invalid baseURL before conditionally setting from model.api.url + +### 1.2 Testing + +- [x] 1.2.1 Verify fix compiles (typecheck passes) +- [x] 1.2.2 Manual test with Anthropic provider +- [x] 1.2.3 Confirm SDK uses default URL when invalid baseURL is filtered + +### 1.3 Documentation + +- [x] 1.3.1 Create openspec proposal documenting the issue +- [ ] 1.3.2 Submit PR with clear description + +## Phase 2: Config-Time Validation + +### 2.1 Design + +- [x] 2.1.1 Research how config validation works (Zod schemas in config.ts) +- [x] 2.1.2 Research models.dev data structure and caching +- [x] 2.1.3 Design hybrid validation strategy (models.dev + URL fallback) +- [x] 2.1.4 Document validation flow diagram + +### 2.2 Implementation + +- [x] 2.2.1 Add `validateProviderConfig()` function to config.ts +- [x] 2.2.2 Implement models.dev cache lookup for validation +- [x] 2.2.3 Implement URL format fallback validation +- [x] 2.2.4 Integrate validation into `loadFile()` after Zod parse +- [x] 2.2.5 Add clear error messages with hints + +### 2.3 Testing + +- [x] 2.3.1 Test with invalid `api` value (e.g., `"anthropic"`) +- [x] 2.3.2 Test with empty string `api` value +- [x] 2.3.3 Test with valid URL `api` value +- [x] 2.3.4 Test fallback when models.dev cache unavailable +- [x] 2.3.5 Verify error messages are helpful + +### 2.4 Documentation + +- [x] 2.4.1 Update proposal with implementation details +- [x] 2.4.2 Update spec with config validation requirements + +--- + +## Validation Flow Diagram + +``` +┌─────────────────────────────────────────────────────────┐ +│ Config Validation Flow │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. Load config file (JSONC parse) │ +│ │ │ +│ ▼ │ +│ 2. Zod schema validation (Info.safeParse) │ +│ │ │ +│ ▼ │ +│ 3. Post-validation hook (NEW) │ +│ ┌───────────────┴───────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ Try models.dev cache Cache unavailable? │ +│ │ │ │ +│ ▼ ▼ │ +│ Validate against URL format check │ +│ known providers (http/https prefix) │ +│ │ │ │ +│ └───────────────┬───────────────┘ │ +│ ▼ │ +│ 4. Return validated config or throw error │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Data Flow Diagram + +``` +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ opencode.json │ │ models.dev │ │ Provider SDK │ +│ │ │ (cached) │ │ │ +│ provider: │ │ │ │ baseURL: │ +│ anthropic: │ │ anthropic: │ │ ?? default │ +│ api: ??? │────▶│ api: null │────▶│ │ +│ │ │ │ │ │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ + User Config Reference Data Runtime Check + (may be wrong) (what's valid) (last defense) +```