feat: add OpenRouter provider preference support

Implement provider preference feature for OpenRouter models:

1. Database Schema:
   - Add ModelProviderPreferenceTable to store provider preferences per model

2. Console Core API:
   - Add ProviderPreferenceSchema for validation
   - Add setProviderPreference(), getProviderPreference(), deleteProviderPreference(), and listProviderPreferences() functions

3. Request Handler:
   - Modify handler.ts to fetch provider preferences from database
   - Pass provider preferences to OpenRouter in request body

4. Admin UI:
   - Add provider preference dropdown for OpenRouter models in model-section.tsx
   - Add 'Allow fallbacks' checkbox for fallback control
   - Add corresponding CSS styles

5. CLI Config:
   - Add providerPreference field to Provider model config schema
   - Support configuring provider preferences via opencode.json

6. Provider SDK:
   - Inject provider preferences into OpenRouter request body in fetch wrapper

This allows workspace admins to specify preferred providers for OpenRouter
models, ensuring models like Claude route through specific providers
(e.g., Anthropic direct) rather than third-party providers.
This commit is contained in:
southseact-3d
2026-02-19 14:18:58 +00:00
parent 5627f7d758
commit 25367847ed
7 changed files with 311 additions and 8 deletions

View File

@@ -2,6 +2,7 @@ import { z } from "zod"
import { eq, and } from "drizzle-orm"
import { Database } from "./drizzle"
import { ModelTable } from "./schema/model.sql"
import { ModelProviderPreferenceTable } from "./schema/model-provider-preference.sql"
import { Identifier } from "./identifier"
import { fn } from "./util/fn"
import { Actor } from "./actor"
@@ -91,6 +92,14 @@ export namespace ZenData {
})
}
export const ProviderPreferenceSchema = 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(),
})
export type ProviderPreference = z.infer<typeof ProviderPreferenceSchema>
export namespace Model {
export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
Actor.assertAdmin()
@@ -143,4 +152,77 @@ export namespace Model {
})
},
)
export const setProviderPreference = fn(
z.object({
model: z.string(),
preference: ProviderPreferenceSchema,
}),
({ model, preference }) => {
Actor.assertAdmin()
return Database.use((db) =>
db
.insert(ModelProviderPreferenceTable)
.values({
id: Identifier.create("model_provider_pref"),
workspaceID: Actor.workspace(),
model: model,
provider_preference: JSON.stringify(preference),
})
.onDuplicateKeyUpdate({
set: {
provider_preference: JSON.stringify(preference),
timeDeleted: null,
},
}),
)
},
)
export const getProviderPreference = fn(
z.object({ model: z.string() }),
({ model }) => {
return Database.use(async (db) => {
const result = await db
.select({ preference: ModelProviderPreferenceTable.provider_preference })
.from(ModelProviderPreferenceTable)
.where(and(eq(ModelProviderPreferenceTable.workspaceID, Actor.workspace()), eq(ModelProviderPreferenceTable.model, model)))
.limit(1)
if (result.length === 0) return undefined
try {
return ProviderPreferenceSchema.parse(JSON.parse(result[0].preference))
} catch {
return undefined
}
})
},
)
export const deleteProviderPreference = fn(
z.object({ model: z.string() }),
({ model }) => {
Actor.assertAdmin()
return Database.use((db) =>
db.delete(ModelProviderPreferenceTable).where(and(eq(ModelProviderPreferenceTable.workspaceID, Actor.workspace()), eq(ModelProviderPreferenceTable.model, model))),
)
},
)
export const listProviderPreferences = fn(z.void(), () => {
return Database.use(async (db) => {
const results = await db
.select({ model: ModelProviderPreferenceTable.model, preference: ModelProviderPreferenceTable.provider_preference })
.from(ModelProviderPreferenceTable)
.where(eq(ModelProviderPreferenceTable.workspaceID, Actor.workspace()))
const parsed: Record<string, ProviderPreference> = {}
for (const row of results) {
try {
parsed[row.model] = ProviderPreferenceSchema.parse(JSON.parse(row.preference))
} catch {}
}
return parsed
})
})
}

View File

@@ -0,0 +1,14 @@
import { mysqlTable, varchar, text, integer, uniqueIndex } from "drizzle-orm/mysql-core"
import { timestamps, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const ModelProviderPreferenceTable = mysqlTable(
"model_provider_preference",
{
...workspaceColumns,
...timestamps,
model: varchar("model", { length: 128 }).notNull(),
provider_preference: text("provider_preference").notNull(),
},
(table) => [...workspaceIndexes(table), uniqueIndex("model_provider_preference_workspace_model").on(table.workspaceID, table.model)],
)