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:
@@ -171,3 +171,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.providerForm {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallbackLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:disabled) {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Model } from "@opencode-ai/console-core/model.js"
|
import { Model, ZenData, ProviderPreference } from "@opencode-ai/console-core/model.js"
|
||||||
import { query, action, useParams, createAsync, json } from "@solidjs/router"
|
import { query, action, useParams, createAsync, json } from "@solidjs/router"
|
||||||
import { createMemo, For, Show } from "solid-js"
|
import { createMemo, For, Show, createSignal, createEffect } from "solid-js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
|
||||||
import styles from "./model-section.module.css"
|
import styles from "./model-section.module.css"
|
||||||
import { querySessionInfo } from "../common"
|
import { querySessionInfo } from "../common"
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +19,18 @@ import { useI18n } from "~/context/i18n"
|
|||||||
import { useLanguage } from "~/context/language"
|
import { useLanguage } from "~/context/language"
|
||||||
import { formError } from "~/lib/form-error"
|
import { formError } from "~/lib/form-error"
|
||||||
|
|
||||||
|
const OPENROUTER_PROVIDERS = [
|
||||||
|
{ value: "", label: "Auto (default)" },
|
||||||
|
{ value: "anthropic", label: "Anthropic only" },
|
||||||
|
{ value: "openai", label: "OpenAI only" },
|
||||||
|
{ value: "google", label: "Google only" },
|
||||||
|
{ value: "together", label: "Together AI only" },
|
||||||
|
{ value: "fireworks", label: "Fireworks AI only" },
|
||||||
|
{ value: "groq", label: "Groq only" },
|
||||||
|
{ value: "deepinfra", label: "DeepInfra only" },
|
||||||
|
{ value: "perplexity", label: "Perplexity only" },
|
||||||
|
]
|
||||||
|
|
||||||
const getModelLab = (modelId: string) => {
|
const getModelLab = (modelId: string) => {
|
||||||
if (modelId.startsWith("claude")) return "Anthropic"
|
if (modelId.startsWith("claude")) return "Anthropic"
|
||||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||||
@@ -35,8 +46,10 @@ const getModelLab = (modelId: string) => {
|
|||||||
const getModelsInfo = query(async (workspaceID: string) => {
|
const getModelsInfo = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
|
const zenData = ZenData.list()
|
||||||
|
const preferences = await Model.listProviderPreferences()
|
||||||
return {
|
return {
|
||||||
all: Object.entries(ZenData.list().models)
|
all: Object.entries(zenData.models)
|
||||||
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
||||||
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
||||||
.sort(([idA, modelA], [idB, modelB]) => {
|
.sort(([idA, modelA], [idB, modelB]) => {
|
||||||
@@ -53,7 +66,16 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||||||
const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
|
const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
|
||||||
return modelAName.localeCompare(modelBName)
|
return modelAName.localeCompare(modelBName)
|
||||||
})
|
})
|
||||||
.map(([id, model]) => ({ id, name: Array.isArray(model) ? model[0].name : model.name })),
|
.map(([id, model]) => {
|
||||||
|
const modelData = Array.isArray(model) ? model[0] : model
|
||||||
|
const hasOpenRouter = modelData.providers.some((p) => p.id === "openrouter")
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: modelData.name,
|
||||||
|
hasOpenRouter,
|
||||||
|
providerPreference: preferences[id],
|
||||||
|
}
|
||||||
|
}),
|
||||||
disabled: await Model.listDisabled(),
|
disabled: await Model.listDisabled(),
|
||||||
}
|
}
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
@@ -78,6 +100,33 @@ const updateModel = action(async (form: FormData) => {
|
|||||||
)
|
)
|
||||||
}, "model.toggle")
|
}, "model.toggle")
|
||||||
|
|
||||||
|
const updateProviderPreference = action(async (form: FormData) => {
|
||||||
|
"use server"
|
||||||
|
const model = form.get("model")?.toString()
|
||||||
|
if (!model) return { error: formError.modelRequired }
|
||||||
|
const workspaceID = form.get("workspaceID")?.toString()
|
||||||
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
|
const providerValue = form.get("provider")?.toString()
|
||||||
|
const allowFallbacks = form.get("allowFallbacks")?.toString() === "true"
|
||||||
|
|
||||||
|
return json(
|
||||||
|
withActor(async () => {
|
||||||
|
if (!providerValue) {
|
||||||
|
await Model.deleteProviderPreference({ model })
|
||||||
|
} else {
|
||||||
|
await Model.setProviderPreference({
|
||||||
|
model,
|
||||||
|
preference: {
|
||||||
|
order: [providerValue],
|
||||||
|
allow_fallbacks: allowFallbacks,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, workspaceID),
|
||||||
|
{ revalidate: getModelsInfo.key },
|
||||||
|
)
|
||||||
|
}, "model.providerPreference")
|
||||||
|
|
||||||
export function ModelSection() {
|
export function ModelSection() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
@@ -110,13 +159,16 @@ export function ModelSection() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{i18n.t("workspace.models.table.model")}</th>
|
<th>{i18n.t("workspace.models.table.model")}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th>Provider</th>
|
||||||
<th>{i18n.t("workspace.models.table.enabled")}</th>
|
<th>{i18n.t("workspace.models.table.enabled")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={modelsWithLab()}>
|
<For each={modelsWithLab()}>
|
||||||
{({ id, name, lab }) => {
|
{({ id, name, lab, hasOpenRouter, providerPreference }) => {
|
||||||
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
|
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
|
||||||
|
const currentProvider = () => providerPreference?.order?.[0] ?? ""
|
||||||
|
const allowFallbacks = () => providerPreference?.allow_fallbacks ?? true
|
||||||
return (
|
return (
|
||||||
<tr data-slot="model-row" data-disabled={!isEnabled()}>
|
<tr data-slot="model-row" data-disabled={!isEnabled()}>
|
||||||
<td data-slot="model-name">
|
<td data-slot="model-name">
|
||||||
@@ -147,6 +199,43 @@ export function ModelSection() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-slot="model-lab">{lab}</td>
|
<td data-slot="model-lab">{lab}</td>
|
||||||
|
<td data-slot="model-provider">
|
||||||
|
<Show when={hasOpenRouter}>
|
||||||
|
<form action={updateProviderPreference} method="post" class={styles.providerForm}>
|
||||||
|
<input type="hidden" name="model" value={id} />
|
||||||
|
<input type="hidden" name="workspaceID" value={params.id} />
|
||||||
|
<select
|
||||||
|
name="provider"
|
||||||
|
disabled={!userInfo()?.isAdmin}
|
||||||
|
onChange={(e) => {
|
||||||
|
const form = e.currentTarget.closest("form")
|
||||||
|
if (form) form.requestSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={OPENROUTER_PROVIDERS}>
|
||||||
|
{(option) => (
|
||||||
|
<option value={option.value} selected={currentProvider() === option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<label class={styles.fallbackLabel}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="allowFallbacks"
|
||||||
|
checked={allowFallbacks()}
|
||||||
|
disabled={!userInfo()?.isAdmin || !currentProvider()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const form = e.currentTarget.closest("form")
|
||||||
|
if (form) form.requestSubmit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>Allow fallbacks</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
<td data-slot="model-toggle">
|
<td data-slot="model-toggle">
|
||||||
<form action={updateModel} method="post">
|
<form action={updateModel} method="post">
|
||||||
<input type="hidden" name="model" value={id} />
|
<input type="hidden" name="model" value={id} />
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
|||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
import { ZenData, Model, ProviderPreference } from "@opencode-ai/console-core/model.js"
|
||||||
import { Black, BlackData } from "@opencode-ai/console-core/black.js"
|
import { Black, BlackData } from "@opencode-ai/console-core/black.js"
|
||||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
||||||
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
||||||
|
import { ModelProviderPreferenceTable } from "@opencode-ai/console-core/schema/model-provider-preference.sql.js"
|
||||||
import { logger } from "./logger"
|
import { logger } from "./logger"
|
||||||
import {
|
import {
|
||||||
AuthError,
|
AuthError,
|
||||||
@@ -85,6 +86,7 @@ export async function handler(
|
|||||||
const stickyProvider = await stickyTracker?.get()
|
const stickyProvider = await stickyTracker?.get()
|
||||||
const authInfo = await authenticate(modelInfo)
|
const authInfo = await authenticate(modelInfo)
|
||||||
const billingSource = validateBilling(authInfo, modelInfo)
|
const billingSource = validateBilling(authInfo, modelInfo)
|
||||||
|
const providerPreference = await getProviderPreference(authInfo, modelInfo.id)
|
||||||
|
|
||||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||||
const providerInfo = selectProvider(
|
const providerInfo = selectProvider(
|
||||||
@@ -107,6 +109,14 @@ export async function handler(
|
|||||||
providerInfo.modifyBody({
|
providerInfo.modifyBody({
|
||||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||||
model: providerInfo.model,
|
model: providerInfo.model,
|
||||||
|
...(providerInfo.id === "openrouter" && providerPreference && {
|
||||||
|
provider: {
|
||||||
|
...(providerPreference.order && { order: providerPreference.order }),
|
||||||
|
...(providerPreference.only && { only: providerPreference.only }),
|
||||||
|
...(providerPreference.ignore && { ignore: providerPreference.ignore }),
|
||||||
|
allow_fallbacks: providerPreference.allow_fallbacks ?? true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
logger.debug("REQUEST URL: " + reqUrl)
|
logger.debug("REQUEST URL: " + reqUrl)
|
||||||
@@ -589,6 +599,29 @@ export async function handler(
|
|||||||
providerInfo.apiKey = authInfo.provider.credentials
|
providerInfo.apiKey = authInfo.provider.credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getProviderPreference(authInfo: AuthInfo, modelId: string): Promise<ProviderPreference | undefined> {
|
||||||
|
if (!authInfo) return undefined
|
||||||
|
try {
|
||||||
|
const result = await Database.use((db) =>
|
||||||
|
db
|
||||||
|
.select({ preference: ModelProviderPreferenceTable.provider_preference })
|
||||||
|
.from(ModelProviderPreferenceTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(ModelProviderPreferenceTable.workspaceID, authInfo.workspaceID),
|
||||||
|
eq(ModelProviderPreferenceTable.model, modelId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.length === 0) return undefined
|
||||||
|
return JSON.parse(result[0].preference) as ProviderPreference
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function trackUsage(
|
async function trackUsage(
|
||||||
authInfo: AuthInfo,
|
authInfo: AuthInfo,
|
||||||
modelInfo: ModelInfo,
|
modelInfo: ModelInfo,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod"
|
|||||||
import { eq, and } from "drizzle-orm"
|
import { eq, and } from "drizzle-orm"
|
||||||
import { Database } from "./drizzle"
|
import { Database } from "./drizzle"
|
||||||
import { ModelTable } from "./schema/model.sql"
|
import { ModelTable } from "./schema/model.sql"
|
||||||
|
import { ModelProviderPreferenceTable } from "./schema/model-provider-preference.sql"
|
||||||
import { Identifier } from "./identifier"
|
import { Identifier } from "./identifier"
|
||||||
import { fn } from "./util/fn"
|
import { fn } from "./util/fn"
|
||||||
import { Actor } from "./actor"
|
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 namespace Model {
|
||||||
export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
|
export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
|
||||||
Actor.assertAdmin()
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)],
|
||||||
|
)
|
||||||
@@ -1030,6 +1030,15 @@ export namespace Config {
|
|||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Variant-specific configuration"),
|
.describe("Variant-specific configuration"),
|
||||||
|
providerPreference: z
|
||||||
|
.object({
|
||||||
|
order: z.array(z.string()).optional().describe("Provider routing order (e.g., ['anthropic', 'openai'])"),
|
||||||
|
only: z.array(z.string()).optional().describe("Whitelist of providers to use"),
|
||||||
|
ignore: z.array(z.string()).optional().describe("Blacklist of providers to skip"),
|
||||||
|
allow_fallbacks: z.boolean().optional().describe("Allow fallback to other providers (default: true)"),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("OpenRouter provider preference for this model"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -940,7 +940,7 @@ export namespace Provider {
|
|||||||
write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
|
write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
|
options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}, model.providerPreference ? { providerPreference: model.providerPreference } : {}),
|
||||||
limit: {
|
limit: {
|
||||||
context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
|
context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
|
||||||
output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
|
output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
|
||||||
@@ -1177,6 +1177,27 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject OpenRouter provider preference for OpenRouter requests
|
||||||
|
if (model.providerID === "openrouter" && opts.body && opts.method === "POST") {
|
||||||
|
const providerPref = model.options?.providerPreference as {
|
||||||
|
order?: string[]
|
||||||
|
only?: string[]
|
||||||
|
ignore?: string[]
|
||||||
|
allow_fallbacks?: boolean
|
||||||
|
} | undefined
|
||||||
|
|
||||||
|
if (providerPref && (providerPref.order || providerPref.only || providerPref.ignore)) {
|
||||||
|
const body = JSON.parse(opts.body as string)
|
||||||
|
body.provider = {
|
||||||
|
...(providerPref.order && { order: providerPref.order }),
|
||||||
|
...(providerPref.only && { only: providerPref.only }),
|
||||||
|
...(providerPref.ignore && { ignore: providerPref.ignore }),
|
||||||
|
allow_fallbacks: providerPref.allow_fallbacks ?? true,
|
||||||
|
}
|
||||||
|
opts.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fetchFn(input, {
|
return fetchFn(input, {
|
||||||
...opts,
|
...opts,
|
||||||
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
||||||
|
|||||||
Reference in New Issue
Block a user