From 00ad20278e00e577ceba587e0a3f8e298813e24b Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Wed, 18 Feb 2026 19:52:32 +0000 Subject: [PATCH] templates and openrouter plan provider preference doc --- PLAN_PROVIDER_PREFERENCE.md | 698 ++++++++++++++++++++++++++++++++++ chat/templates/templates.json | 74 +++- 2 files changed, 756 insertions(+), 16 deletions(-) create mode 100644 PLAN_PROVIDER_PREFERENCE.md diff --git a/PLAN_PROVIDER_PREFERENCE.md b/PLAN_PROVIDER_PREFERENCE.md new file mode 100644 index 0000000..7ed3fad --- /dev/null +++ b/PLAN_PROVIDER_PREFERENCE.md @@ -0,0 +1,698 @@ +# Provider Preference per Model - Implementation Plan + +## Overview + +This document outlines the implementation plan for allowing users to specify preferred providers for specific models when using OpenRouter via the OpenCode CLI. For example, ensuring Claude models only route through Anthropic's direct API rather than third-party providers. + +--- + +## Part 1: OpenRouter API Research + +### How Provider Routing Works + +OpenRouter supports a `provider` object in the request body for Chat Completions that controls routing behavior: + +```json +{ + "model": "anthropic/claude-sonnet-4", + "messages": [...], + "provider": { + "order": ["anthropic"], + "allow_fallbacks": false + } +} +``` + +### Key Provider Routing Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `order` | `string[]` | List of provider slugs to try in order (e.g., `["anthropic", "openai"]`) | +| `allow_fallbacks` | `boolean` | Whether to allow backup providers when primary is unavailable (default: `true`) | +| `only` | `string[]` | List of provider slugs to allow for this request (whitelist) | +| `ignore` | `string[]` | List of provider slugs to skip for this request (blacklist) | +| `sort` | `string \| object` | Sort providers by `"price"`, `"throughput"`, or `"latency"` | +| `require_parameters` | `boolean` | Only use providers that support all request parameters | +| `data_collection` | `"allow" \| "deny"` | Control whether to use providers that may store data | +| `zdr` | `boolean` | Restrict to Zero Data Retention endpoints | + +### Common Use Cases + +**Force specific provider only:** +```json +{ + "provider": { + "order": ["anthropic"], + "allow_fallbacks": false + } +} +``` + +**Prefer provider but allow fallbacks:** +```json +{ + "provider": { + "order": ["anthropic"] + } +} +``` + +**Whitelist multiple providers:** +```json +{ + "provider": { + "only": ["anthropic", "openai", "google"] + } +} +``` + +**Blacklist specific providers:** +```json +{ + "provider": { + "ignore": ["some-provider", "another-provider"] + } +} +``` + +### Available Provider Slugs (Examples) + +- `anthropic` - Anthropic direct +- `openai` - OpenAI direct +- `google` - Google AI Studio +- `azure` - Azure OpenAI +- `together` - Together AI +- `fireworks` - Fireworks AI +- `groq` - Groq +- `deepinfra` - DeepInfra +- `bedrock` - AWS Bedrock +- `vertex-ai` - Google Vertex AI + +--- + +## Part 2: Current Architecture Analysis + +### Relevant Files + +| File | Purpose | +|------|---------| +| `packages/console/core/src/model.ts` | ZenData model configuration with providers array | +| `packages/console/app/src/routes/zen/util/handler.ts` | Request handler with provider selection logic | +| `packages/console/app/src/routes/workspace/[id]/model-section.tsx` | Admin UI for model settings | +| `packages/app/src/components/settings-models.tsx` | Desktop app model visibility settings | +| `packages/opencode/src/provider/models.ts` | CLI model definitions from models.dev | + +### Current Provider Selection Logic + +The `selectProvider` function in `handler.ts` (lines 340-398) currently selects providers based on: + +1. **BYOK (Bring Your Own Key)** - Uses provider credentials if configured +2. **Trial mode** - Routes to trial provider if applicable +3. **Sticky provider** - Maintains same provider across session +4. **Weighted random selection** - Distributes across providers based on weight + +### Model Configuration Schema + +From `packages/console/core/src/model.ts`: + +```typescript +const ModelSchema = z.object({ + name: z.string(), + cost: ModelCostSchema, + providers: z.array(z.object({ + id: z.string(), // Provider slug + model: z.string(), // Model name for provider + weight: z.number().optional(), + disabled: z.boolean().optional(), + })), + stickyProvider: z.enum(["strict", "prefer"]).optional(), + fallbackProvider: z.string().optional(), + byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), + // ... +}) +``` + +--- + +## Part 3: Implementation Options + +### Option A: Model-Level Provider Preference (Server-Side) + +Add provider preference to model configuration in ZenData, allowing workspace admins to set preferred providers. + +**Pros:** +- Centralized control +- Applied automatically to all requests +- No client-side changes required + +**Cons:** +- Requires server deployment for changes +- Not immediately visible to end users + +### Option B: Request-Level Provider Preference (Client-Side) + +Allow clients to specify provider preferences per request via headers or request body. + +**Pros:** +- Flexible for different use cases +- No server deployment needed for preference changes + +**Cons:** +- Requires client library updates +- More complex API surface + +### Option C: Hybrid Approach (Recommended) + +Combine both: server-level defaults with client-side overrides. + +--- + +## Part 4: Implementation Details + +### 4.1 Database Schema Changes + +Add new table for model provider preferences: + +```sql +-- packages/console/core/src/schema/model-provider.sql.ts + +import { text, integer, mysqlTable } from "drizzle-orm/mysql-core" + +export const ModelProviderTable = mysqlTable("model_provider", { + id: text("id").primaryKey(), + workspace_id: text("workspace_id").notNull(), + model: text("model").notNull(), + provider_preference: text("provider_preference").notNull(), // JSON: { order: [], allow_fallbacks: bool, only: [], ignore: [] } + time_created: integer("time_created").notNull(), + time_updated: integer("time_updated"), +}) +``` + +### 4.2 API Changes + +Add new functions to model.ts: + +```typescript +export namespace Model { + // Existing functions... + + export const setProviderPreference = fn( + z.object({ + model: z.string(), + preference: z.object({ + order: z.array(z.string()).optional(), + only: z.array(z.string()).optional(), + ignore: z.array(z.string()).optional(), + allow_fallbacks: z.boolean().optional(), + }), + }), + ({ model, preference }) => { + Actor.assertAdmin() + // Store preference in database + } + ) + + export const getProviderPreference = fn( + z.object({ model: z.string() }), + ({ model }) => { + // Retrieve preference from database + } + ) +} +``` + +### 4.3 Handler Changes + +Modify `handler.ts` to apply provider preferences: + +```typescript +function selectProvider( + reqModel: string, + zenData: ZenData, + authInfo: AuthInfo, + modelInfo: ModelInfo, + sessionId: string, + isTrial: boolean, + retry: RetryOptions, + stickyProvider: string | undefined, + userPreference: ProviderPreference | undefined, // NEW +) { + // Apply user preference first + if (userPreference?.order && userPreference.order.length > 0) { + const preferredProvider = userPreference.order + .map(slug => modelInfo.providers.find(p => p.id === slug)) + .find(p => p && !p.disabled) + + if (preferredProvider) { + if (!userPreference.allow_fallbacks) { + return preferredProvider + } + // Continue with fallback logic... + } + } + + // Existing logic... +} +``` + +### 4.4 Request Body Forwarding + +For OpenRouter integration, pass provider preferences in the request body: + +```typescript +const reqBody = JSON.stringify({ + ...createBodyConverter(opts.format, providerInfo.format)(body), + model: providerInfo.model, + // Add provider preferences if using OpenRouter + ...(providerInfo.id === 'openrouter' && userPreference && { + provider: { + order: userPreference.order, + allow_fallbacks: userPreference.allow_fallbacks ?? true, + only: userPreference.only, + ignore: userPreference.ignore, + } + }), +}) +``` + +--- + +## Part 5: Admin UI Options + +### Option 1: Inline Provider Dropdown (Recommended) + +Add a dropdown next to each model in the existing table: + +```tsx + + +
{/* Model icon and name */}
+ + + + + + {/* Enable/disable toggle */} + + +``` + +### Option 2: Expandable Model Details + +Add expandable row with detailed provider settings: + +```tsx + + {/* Basic info */} + + + +
+

Provider Preferences

+
+ + +
+
+ + +
+
+ + +
+
+ + +``` + +### Option 3: Dedicated Provider Settings Page + +Create a new tab/modal for advanced provider configuration: + +```tsx +// New route: /workspace/[id]/models/[modelId]/providers + +export function ModelProviderSettings() { + const params = useParams() + const modelInfo = createAsync(() => getModelInfo(params.id, params.modelId)) + + return ( +
+
+

{modelInfo()?.name} - Provider Settings

+
+ +
+

Routing Strategy

+ + Automatic (OpenRouter optimized) + Prefer provider + Require provider + +
+ +
+

Provider Priority

+ + {(provider) => ( +
+ + {provider.name} + +
+ )} +
+
+ +
+

Fallback Behavior

+ + Allow fallback to other providers + +
+ +
+

Excluded Providers

+ +
+
+ ) +} +``` + +### Option 4: Compact Status Indicators + +Add visual indicators for provider preference status: + +```tsx + +
+ + + + {providerPreference().allow_fallbacks ? '' : ' (strict)'} + + + + Auto + +
+ +``` + +### Option 5: Bulk Configuration Modal + +Allow setting preferences for multiple models at once: + +```tsx +export function BulkProviderModal() { + const [selectedModels, setSelectedModels] = createSignal([]) + const [preference, setPreference] = createSignal({}) + + return ( + + + + + + + + ) +} +``` + +--- + +## Part 6: UI/UX Recommendations + +### Recommended Approach: Combination of Options 1 + 4 + +1. **Main Table View**: Add compact provider indicator (Option 4) to show current preference +2. **Quick Change**: Dropdown for common presets (Option 1) +3. **Advanced Settings**: Link to detailed configuration page (Option 3) + +### Visual Design + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Models │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Model │ Provider │ Status │ Enabled │ │ +│ ├─────────────────────────────────────────────────────────────────────┤ │ +│ │ 🟣 Claude 4 │ [Anthropic ▼] │ ● │ [====] │ │ +│ │ 🟢 GPT-4o │ [Auto ▼] │ Auto │ [====] │ │ +│ │ 🔵 Gemini 2 │ [Google ▼] │ ● │ [====] │ │ +│ │ 🟡 Grok 2 │ [Auto ▼] │ Auto │ [====] │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 💡 Click provider dropdown to change routing preferences │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Dropdown Options + +Each model's provider dropdown should include: + +1. **Auto (default)** - Use OpenRouter's load balancing +2. **[Provider] only** - Strict provider requirement (no fallbacks) +3. **Prefer [Provider]** - Prefer this provider with fallbacks +4. **Advanced...** - Open detailed settings modal + +--- + +## Part 7: Implementation Phases + +### Phase 1: Backend Foundation (Week 1) +- [ ] Create database schema for provider preferences +- [ ] Add API endpoints for CRUD operations +- [ ] Modify request handler to apply preferences +- [ ] Add tests for provider selection logic + +### Phase 2: Basic UI (Week 2) +- [ ] Add provider column to model settings table +- [ ] Implement dropdown for common presets +- [ ] Add visual indicators for current preference +- [ ] Wire up API calls to save preferences + +### Phase 3: Advanced Features (Week 3) +- [ ] Create detailed provider settings page +- [ ] Implement drag-drop provider ordering +- [ ] Add bulk configuration modal +- [ ] Add provider blacklist/whitelist UI + +### Phase 4: Polish & Testing (Week 4) +- [ ] Add loading states and error handling +- [ ] Implement optimistic updates +- [ ] Add comprehensive tests +- [ ] Documentation updates + +--- + +## Part 8: Testing Strategy + +### Unit Tests + +```typescript +// packages/console/core/src/model.test.ts + +describe("Model.setProviderPreference", () => { + it("should store provider preference for model", async () => { + const result = await Model.setProviderPreference({ + model: "claude-sonnet-4", + preference: { + order: ["anthropic"], + allow_fallbacks: false, + }, + }) + expect(result).toBeDefined() + }) +}) + +describe("Provider selection with preference", () => { + it("should use preferred provider when set", () => { + const provider = selectProvider(/* ... */, { + order: ["anthropic"], + allow_fallbacks: false, + }) + expect(provider.id).toBe("anthropic") + }) + + it("should fallback when allow_fallbacks is true", () => { + // Mock anthropic provider as disabled + const provider = selectProvider(/* ... */, { + order: ["anthropic"], + allow_fallbacks: true, + }) + expect(provider.id).not.toBe("anthropic") + }) +}) +``` + +### Integration Tests + +```typescript +// packages/console/app/src/routes/zen/v1/chat/completions.test.ts + +describe("Chat completions with provider preference", () => { + it("should route to preferred provider", async () => { + // Set preference + await Model.setProviderPreference({ + model: "claude-sonnet-4", + preference: { order: ["anthropic"], allow_fallbacks: false }, + }) + + // Make request + const response = await fetch("/api/v1/chat/completions", { + method: "POST", + body: JSON.stringify({ + model: "claude-sonnet-4", + messages: [{ role: "user", content: "Hello" }], + }), + }) + + // Verify it went to Anthropic + expect(response.status).toBe(200) + }) +}) +``` + +--- + +## Part 9: Security Considerations + +### Access Control +- Only workspace admins can modify provider preferences +- Validate provider slugs against allowed list +- Prevent injection of malicious provider IDs + +### Data Validation +```typescript +const ALLOWED_PROVIDERS = [ + "anthropic", "openai", "google", "azure", "together", + "fireworks", "groq", "deepinfra", "bedrock", "vertex-ai", + // ... add as needed +] + +const ProviderPreferenceSchema = z.object({ + order: z.array(z.enum(ALLOWED_PROVIDERS)).optional(), + only: z.array(z.enum(ALLOWED_PROVIDERS)).optional(), + ignore: z.array(z.enum(ALLOWED_PROVIDERS)).optional(), + allow_fallbacks: z.boolean().optional(), +}) +``` + +--- + +## Part 10: Documentation Needs + +1. **User Documentation** + - How to set provider preferences + - When to use strict vs. fallback mode + - Provider-specific considerations + +2. **API Documentation** + - New endpoints for provider preferences + - Request/response schemas + - Error codes + +3. **Internal Documentation** + - Architecture decisions + - Provider selection algorithm + - Database schema + +--- + +## Appendix A: Provider Slug Reference + +| Provider | Slug | Notes | +|----------|------|-------| +| Anthropic | `anthropic` | Direct API | +| OpenAI | `openai` | Direct API | +| Google AI Studio | `google` | Direct API | +| Google Vertex AI | `vertex-ai` | Enterprise | +| AWS Bedrock | `bedrock` | Enterprise | +| Azure OpenAI | `azure` | Enterprise | +| Together AI | `together` | Third-party | +| Fireworks AI | `fireworks` | Third-party | +| Groq | `groq` | High-speed | +| DeepInfra | `deepinfra` | Cost-effective | +| Replicate | `replicate` | Various models | +| Perplexity | `perplexity` | Online models | + +--- + +## Appendix B: Related Files + +### Files to Modify +- `packages/console/core/src/model.ts` - Add preference functions +- `packages/console/core/src/schema/model-provider.sql.ts` - New schema +- `packages/console/app/src/routes/zen/util/handler.ts` - Apply preferences +- `packages/console/app/src/routes/workspace/[id]/model-section.tsx` - UI changes +- `packages/console/app/src/i18n/en.ts` - Add translations + +### Files to Create +- `packages/console/core/src/schema/model-provider.sql.ts` - Database schema +- `packages/console/app/src/routes/workspace/[id]/model-provider-settings.tsx` - Advanced UI +- `packages/console/app/src/component/provider-preference-dropdown.tsx` - Reusable component + +--- + +## Appendix C: OpenRouter API Reference + +Full provider object schema from OpenRouter API: + +```json +{ + "provider": { + "order": ["string"], + "allow_fallbacks": true, + "require_parameters": false, + "data_collection": "allow", + "zdr": false, + "enforce_distillable_text": false, + "only": ["string"], + "ignore": ["string"], + "quantizations": ["int4", "int8", "fp8", "fp16", "bf16", "fp32"], + "sort": "price | throughput | latency", + "preferred_min_throughput": 0, + "preferred_max_latency": 0, + "max_price": { + "prompt": 0, + "completion": 0 + } + } +} +``` diff --git a/chat/templates/templates.json b/chat/templates/templates.json index ca1c00c..0318d1f 100644 --- a/chat/templates/templates.json +++ b/chat/templates/templates.json @@ -1,23 +1,65 @@ [ { - "id": "woocommerce-seo-booster", - "name": "WooCommerce SEO Booster", - "description": "A comprehensive starting point for WooCommerce SEO plugins. Includes meta tags, schema markup, and sitemap generation scaffolding.", - "image": "/assets/templates/seo-booster.jpg", + "id": "announcements", + "name": "Site Announcement Banners", + "description": "Create and manage announcement banners that display at the top of your site with scheduling capabilities. Features admin management, scheduling, and responsive design.", + "image": "/chat/templates/images/announcements.jpg", + "category": "Content" + }, + { + "id": "change-login-url", + "name": "Secure Login URL", + "description": "Hide your WordPress login page with a custom URL slug to improve security and prevent automated brute force attacks.", + "image": "/chat/templates/images/change-login-url.jpg", + "category": "Security" + }, + { + "id": "changelog", + "name": "Product Changelog Display", + "description": "Showcase your product updates with a beautiful changelog page. Custom post type, shortcode support, and automatic page creation.", + "image": "/chat/templates/images/changelog.jpg", + "category": "Content" + }, + { + "id": "community-suggestions", + "name": "Feature Request Board", + "description": "Let your community submit and vote on feature suggestions. Perfect for gathering user feedback and prioritizing development.", + "image": "/chat/templates/images/community-suggestions.jpg", + "category": "Community" + }, + { + "id": "faq-manager", + "name": "FAQ Page Builder", + "description": "Build beautiful FAQ pages with drag-and-drop reordering. Includes accordion styling, accessibility support, and automatic page creation.", + "image": "/chat/templates/images/faq-manager.jpg", + "category": "Content" + }, + { + "id": "form-builder", + "name": "Drag & Drop Form Builder", + "description": "Create custom forms with an intuitive drag-and-drop interface. Track submissions, manage responses, and embed forms anywhere.", + "image": "/chat/templates/images/form-builder.jpg", + "category": "Forms" + }, + { + "id": "headers-footers", + "name": "Header & Footer Scripts", + "description": "Easily add custom code to your site's header and footer. Perfect for analytics tracking, ad pixels, and custom JavaScript.", + "image": "/chat/templates/images/headers-footers.jpg", + "category": "Utilities" + }, + { + "id": "membership", + "name": "Membership & Subscriptions", + "description": "Monetize your content with membership plans. Stripe-powered payments, subscription management, and content access control.", + "image": "/chat/templates/images/membership.jpg", "category": "E-commerce" }, { - "id": "elementor-addon-starter", - "name": "Elementor Addon Starter", - "description": "Boilerplate for creating custom Elementor widgets. Comes with a basic hello-world widget and controls setup.", - "image": "/assets/templates/elementor-starter.jpg", - "category": "Page Builder" - }, - { - "id": "admin-dashboard-widget", - "name": "Admin Dashboard Widget", - "description": "Add custom widgets to the WordPress admin dashboard. Useful for client reports or quick execute actions.", - "image": "/assets/templates/admin-widget.jpg", - "category": "Admin" + "id": "scroll-to-top", + "name": "Back to Top Button", + "description": "Add a customizable scroll-to-top button to your site. Choose styles, colors, positions, and enable/disable on mobile devices.", + "image": "/chat/templates/images/scroll-to-top.jpg", + "category": "Utilities" } ]