diff --git a/CHANGELOG.md b/CHANGELOG.md index ef9c19a..1ebe5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project are documented in this file. +## [1.1.1] - 2026-05-12 + +### Fixed + +- Fixed combo model processing when the API returns model entries as objects instead of strings in the `models` array. (@hjasgr) +- Hardened `resolveUnderlyingModels` against null/undefined entries and missing properties with runtime type narrowing. + +### Changed + +- Updated `OmniRouteCombo.models` type to accept objects from the API. + +## [1.1.0] - 2026-03-10 + +### Added + +- Added models.dev capability enrichment for OmniRoute models via `provider.omniroute.options.modelsDev`. +- Added combo model support with automatic lowest-common-denominator capability calculation from `/api/combos`. +- Added `modelMetadata` configuration option for custom/virtual model overrides via `provider.omniroute.options.modelMetadata`. +- Added new runtime exports: `clearModelsDevCache`, `clearComboCache`, `fetchComboData`, `resolveUnderlyingModels`, `calculateModelCapabilities`. +- Added provider alias mapping (e.g., `cx` → `openai`, `gemini` → `google`) for models.dev lookups. + +### Changed + +- Updated `fetchModels` to orchestrate enrichment pipeline (models.dev → combo capabilities). +- Updated `clearModelCache` to also clear the combo cache. +- Updated README with combo model documentation and new runtime API references. + ## [1.0.3] - 2026-03-01 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e6072af --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,170 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`opencode-omniroute-auth` is an OpenCode authentication plugin for the OmniRoute API. It provides a `/connect omniroute` command, API-key auth, dynamic model fetching from `/v1/models`, and combo model capability enrichment. + +## Common Commands + +```bash +# Build (required before running tests) +npm run build + +# Watch mode during development +npm run dev + +# Run tests (builds first, then runs Node built-in test runner) +npm test + +# Run a single test file +npm run build && node --test test/plugin.test.mjs + +# Type-check a single file without emitting +npx tsc --noEmit src/plugin.ts + +# Clean build output +npm run clean + +# Validate dist exports satisfy plugin loader constraints +npm run check:exports + +# Full publish prep +npm run prepublishOnly +``` + +## Architecture + +### Dual Entry Points + +- **`index.ts`** — Main plugin export (`OmniRouteAuthPlugin`). Required by OpenCode's plugin loader. All root exports must be functions. +- **`runtime.ts`** — Runtime utilities (`fetchModels`, `clearModelCache`, combo helpers, etc.) exported for programmatic use. + +### Core Modules + +| File | Responsibility | +|------|----------------| +| `src/plugin.ts` | Plugin implementation: `config` hook (registers `omniroute` provider), `auth` hook (`/connect` command), `loadProviderOptions` (fetches models and returns a `fetch` interceptor). | +| `src/models.ts` | `fetchModels()` fetches `/v1/models`, manages an in-memory cache keyed by `baseUrl:apiKey`, falls back to defaults on failure. Orchestrates metadata enrichment via `models-dev.ts` and combo enrichment via `omniroute-combos.ts`. | +| `src/models-dev.ts` | Fetches `https://models.dev/api.json`, builds indexed lookup maps (exact/normalized, provider-specific and global), and maps OmniRoute provider keys to models.dev providers via aliases. | +| `src/omniroute-combos.ts` | Fetches combo definitions from `/api/combos`. Resolves underlying models and calculates lowest-common-denominator capabilities (min context/maxTokens, vision/tools only if ALL underlying models support them). | +| `src/constants.ts` | Endpoints, default models, TTLs, timeouts. | +| `src/types.ts` | Shared TypeScript interfaces. | + +### Fetch Interceptor (`createFetchInterceptor` in `src/plugin.ts`) + +The loader returns a `fetch` function that: +1. Adds `Authorization: Bearer ` and `Content-Type: application/json` headers. +2. Only intercepts requests to the configured OmniRoute base URL (with safe prefix matching). +3. Sanitizes Gemini tool schemas by stripping `$schema`, `$ref`, `ref`, and `additionalProperties` keywords when the model name includes "gemini". + +### Caching Strategy + +Three independent in-memory caches: +- **Model cache** (`src/models.ts`) — keyed by `baseUrl:apiKey`, TTL defaults to 5 minutes. +- **models.dev cache** (`src/models-dev.ts`) — global singleton, TTL defaults to 24 hours. +- **Combo cache** (`src/omniroute-combos.ts`) — global singleton, TTL defaults to 5 minutes. + +`clearModelCache()` also clears the combo cache. + +## Code Conventions + +- **ESM only** with `NodeNext` module resolution. Every relative import must use an explicit `.js` extension (e.g., `import { x } from './file.js'`). +- **Import grouping**: external → internal → types. +- **Strict TypeScript** — never use `any`. Prefer `unknown` with runtime narrowing. +- **Runtime validation** before type assertions for external API responses (e.g., validate `typeof rawData === 'object' && Array.isArray(rawData.data)` before casting to `OmniRouteModelsResponse`). +- **Security/logging** — never log API keys or full response bodies. Use the `[OmniRoute]` log prefix. +- **Resource cleanup** — always wrap `fetch` + `AbortController` timeouts in `try/finally` with `clearTimeout`. +- **Naming**: `PascalCase` for types/interfaces, `camelCase` for functions/variables, `UPPER_SNAKE_CASE` for constants, `kebab-case` for files. +- **Formatting**: 2 spaces, max 100 chars/line, semicolons required, single quotes for strings, trailing commas in multi-line objects/arrays. +- **Headers/URLs**: Use the `Headers` constructor for normalization. Handle both `Request` objects and string URLs (`input instanceof Request ? input.url : input.toString()`). When intercepting requests, ensure `baseUrl` ends with `/` for safe prefix matching to prevent domain spoofing. + +## Testing + +Tests use Node.js's built-in test runner (`node:test`, `node:assert/strict`) and import from `dist/`. They mock `global.fetch`. + +- Tests live in `test/*.test.mjs`. +- `npm test` always rebuilds first; if you edit source and rerun a single test file manually, run `npm run build` first. + +## Important Configuration + +- `tsconfig.json` compiles from the root (`rootDir: "."`) and outputs to `./dist`. +- `package.json` `exports` maps `"."` to `dist/index.js` and `"./runtime"` to `dist/runtime.js`. + +## Common Tasks + +- **Adding exports**: Add the export in the source file, re-export it from `index.ts` or `runtime.ts` (with `.js` extension), then run `npm run build`. +- **Debugging**: Look for the `[OmniRoute]` prefix in console logs. + +## Release Process + +### 1. Prepare the version bump + +1. Update `package.json` version. +2. Update `CHANGELOG.md` with a new section for the release. Include the date and credit contributors by GitHub username (e.g., `@username`) when applicable. +3. If there are missing changelog sections for prior releases (e.g., `1.1.0` was released but never documented), add them retroactively so the changelog is complete. +4. Commit the changes: + ```bash + git add package.json CHANGELOG.md + git commit -m "chore: bump version to X.Y.Z" + ``` + +### 2. Merge to `main` + +Ensure the release PR is merged into `main`: +```bash +git checkout main +git pull origin main +``` + +### 3. Tag the release + +Create and push an annotated tag matching the version: +```bash +git tag -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z +``` + +### 4. Create the GitHub Release + +Create a release from the tag using `gh`: +```bash +gh release create vX.Y.Z --title "vX.Y.Z" --notes "$(sed -n '/## \[X.Y.Z\]/,/^## /p' CHANGELOG.md | sed '$d')" +``` + +Or use a prepared notes file if one exists in `docs/`: +```bash +gh release create vX.Y.Z --title "vX.Y.Z" --notes-file docs/release-notes-vX.Y.Z.md +``` + +### 5. Publish to npm + +1. Verify you are logged in: + ```bash + npm whoami + ``` +2. Run the publish prep (clean, build, and export validation): + ```bash + npm run prepublishOnly + ``` +3. Publish: + ```bash + npm publish + ``` + +If npm requires an MFA/2FA OTP, publish with: +```bash +npm publish --otp +``` + +### 6. Post-release verification + +- Confirm the package version on npm: + ```bash + npm view opencode-omniroute-auth version + ``` +- Confirm the GitHub release exists: + ```bash + gh release view vX.Y.Z + ``` \ No newline at end of file diff --git a/package.json b/package.json index ef86a24..61c16bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-omniroute-auth", - "version": "1.1.0", + "version": "1.1.1", "description": "OpenCode authentication plugin for OmniRoute API with /connect command and dynamic model fetching", "type": "module", "main": "./dist/index.js", diff --git a/src/omniroute-combos.ts b/src/omniroute-combos.ts index 617702f..2767d5f 100644 --- a/src/omniroute-combos.ts +++ b/src/omniroute-combos.ts @@ -14,7 +14,7 @@ import { REQUEST_TIMEOUT } from './constants.js'; export interface OmniRouteCombo { id: string; name: string; - models: string[]; + models: Array; strategy: 'priority' | 'weighted' | 'round-robin' | 'random' | 'least-used' | 'cost-optimized'; config: { maxRetries?: number; @@ -139,7 +139,17 @@ export async function resolveUnderlyingModels( const combo = combos.get(modelId); if (combo) { console.log(`[OmniRoute] Resolved combo "${modelId}" to ${combo.models.length} underlying models`); - return combo.models; + return combo.models + .map((m) => { + if (typeof m === 'string') return m; + if (m && typeof m === 'object') { + const modelId = (m as Record).model ?? (m as Record).id; + if (typeof modelId === 'string') return modelId; + } + console.warn('[OmniRoute] Unexpected model entry in combo:', m); + return null; + }) + .filter((m): m is string => m !== null); } // Not a combo, return as-is