diff --git a/.changeset/calm-models-reason.md b/.changeset/calm-models-reason.md new file mode 100644 index 000000000000..49fc92940fe6 --- /dev/null +++ b/.changeset/calm-models-reason.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Support multiple provider APIs, adaptive thinking, split reasoning, and output effort variants for custom providers. diff --git a/packages/kilo-vscode/src/provider-actions.ts b/packages/kilo-vscode/src/provider-actions.ts index 32623f5a643f..739ff339e9d1 100644 --- a/packages/kilo-vscode/src/provider-actions.ts +++ b/packages/kilo-vscode/src/provider-actions.ts @@ -9,7 +9,7 @@ import { sanitizeCustomProviderConfig, withCustomProviderDeletions, } from "./shared/custom-provider" -import { CUSTOM_PROVIDER_PACKAGE, KILO_AUTO, parseModelString } from "./shared/provider-model" +import { isCustomProviderPackage, KILO_AUTO, parseModelString } from "./shared/provider-model" import { configFeatures } from "./features" /** @@ -33,7 +33,7 @@ function record(value: unknown): value is Record { } function customProvider(config: unknown) { - return record(config) && config.npm === CUSTOM_PROVIDER_PACKAGE + return record(config) && isCustomProviderPackage(config.npm) } function same(a: unknown, b: unknown): boolean { diff --git a/packages/kilo-vscode/src/shared/custom-provider.ts b/packages/kilo-vscode/src/shared/custom-provider.ts index 12150b198c08..8cadf7d07a99 100644 --- a/packages/kilo-vscode/src/shared/custom-provider.ts +++ b/packages/kilo-vscode/src/shared/custom-provider.ts @@ -1,5 +1,6 @@ import { z } from "zod" -import { CUSTOM_PROVIDER_PACKAGE, PROVIDER_ID_PATTERN } from "./provider-model" +import { CUSTOM_PROVIDER_PACKAGE, CUSTOM_PROVIDER_PACKAGES, PROVIDER_ID_PATTERN } from "./provider-model" +import type { CustomProviderPackage } from "./provider-model" const INVALID_PROVIDER_ID = "Invalid provider ID" const INVALID_ENV = "Invalid environment variable name" @@ -13,8 +14,10 @@ export const EnvSchema = z const VariantConfigSchema = z.object({ enable_thinking: z.boolean().optional(), - thinking: z.object({ type: z.enum(["enabled", "disabled"]) }).optional(), + thinking: z.object({ type: z.enum(["enabled", "disabled", "adaptive"]) }).optional(), + reasoning_split: z.boolean().optional(), reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(), + effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(), chat_template_args: z.object({ enable_thinking: z.boolean() }).optional(), }) @@ -22,7 +25,7 @@ export type VariantConfig = z.infer export const CustomProviderConfigSchema = z .object({ - npm: z.string().optional(), + npm: z.enum(CUSTOM_PROVIDER_PACKAGES).default(CUSTOM_PROVIDER_PACKAGE), name: z.string().trim().min(1).max(200), env: z.array(EnvSchema).max(1).optional(), options: z @@ -53,7 +56,7 @@ export const CustomProviderConfigSchema = z .strict() export type SanitizedProviderConfig = { - npm: typeof CUSTOM_PROVIDER_PACKAGE + npm: CustomProviderPackage name: string env?: string[] options: { @@ -118,7 +121,7 @@ export function normalizeCustomProviderConfig( : undefined return { - npm: CUSTOM_PROVIDER_PACKAGE, + npm: config.npm, name: config.name.trim(), ...(config.env ? { env: config.env.map((item) => item.trim()) } : {}), options: { @@ -149,13 +152,14 @@ export function sanitizeCustomProviderConfig(provider: unknown): { value: Saniti } type AnyRecord = Record +type VariantPatch = Partial<{ [Key in keyof VariantConfig]: VariantConfig[Key] | null }> type ProviderPatch = Omit & { models: Record< string, null | { name: string reasoning?: true | null - variants?: Record + variants?: Record } > } @@ -165,10 +169,11 @@ function isRecord(v: unknown): v is AnyRecord { } /** - * Build a provider patch that includes null sentinels for model properties - * that existed in the previous config but are absent from the new one. The CLI - * `config.update` endpoint deep-merges the payload with the existing config; - * without explicit nulls, removed entries would persist on disk. + * Build a provider patch that includes null sentinels for model properties, + * variants, and variant options that existed in the previous config but are + * absent from the new one. The CLI `config.update` endpoint deep-merges the + * payload with the existing config; without explicit nulls, removed entries + * would persist on disk. */ export function withCustomProviderDeletions(existing: unknown, next: SanitizedProviderConfig): SanitizedProviderConfig { if (!isRecord(existing)) return next @@ -185,11 +190,20 @@ export function withCustomProviderDeletions(existing: unknown, next: SanitizedPr if (!isRecord(oldModel) || !isRecord(newModel)) continue const oldVariants = isRecord(oldModel.variants) ? oldModel.variants : {} const newVariants = isRecord(newModel.variants) ? newModel.variants : {} - const removed = Object.keys(oldVariants).filter((variant) => !(variant in newVariants)) - const variants = - removed.length > 0 - ? { ...newVariants, ...Object.fromEntries(removed.map((variant) => [variant, null])) } - : newModel.variants + const changes: Record = {} + for (const [name, oldVariant] of Object.entries(oldVariants)) { + if (!(name in newVariants)) { + changes[name] = null + continue + } + const newVariant = newVariants[name] + if (!isRecord(oldVariant) || !isRecord(newVariant)) continue + const removed = Object.keys(oldVariant).filter((key) => !(key in newVariant)) + if (removed.length === 0) continue + const nulls = Object.fromEntries(removed.map((key) => [key, null])) + changes[name] = { ...newVariant, ...nulls } as VariantPatch + } + const variants = Object.keys(changes).length > 0 ? { ...newVariants, ...changes } : newModel.variants patched[id] = { ...newModel, ...(variants ? { variants } : {}), diff --git a/packages/kilo-vscode/src/shared/provider-model.ts b/packages/kilo-vscode/src/shared/provider-model.ts index cb7d6b9c3417..77c74f8ac226 100644 --- a/packages/kilo-vscode/src/shared/provider-model.ts +++ b/packages/kilo-vscode/src/shared/provider-model.ts @@ -1,6 +1,8 @@ export const KILO_PROVIDER_ID = "kilo" export const KILO_AUTO = { providerID: KILO_PROVIDER_ID, modelID: "kilo-auto/free" } as const -export const CUSTOM_PROVIDER_PACKAGE = "@ai-sdk/openai-compatible" +export const CUSTOM_PROVIDER_PACKAGES = ["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"] as const +export type CustomProviderPackage = (typeof CUSTOM_PROVIDER_PACKAGES)[number] +export const CUSTOM_PROVIDER_PACKAGE: CustomProviderPackage = "@ai-sdk/openai-compatible" export const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/ export const PROVIDER_PRIORITY = [ @@ -13,6 +15,10 @@ export const PROVIDER_PRIORITY = [ "vercel", ] as const +export function isCustomProviderPackage(value: unknown): value is CustomProviderPackage { + return CUSTOM_PROVIDER_PACKAGES.includes(value as CustomProviderPackage) +} + export function parseModelString(raw: string | undefined | null) { if (!raw) return null const slash = raw.indexOf("/") diff --git a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts index a045e2e1f763..0ba8b3f4e94b 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts @@ -9,6 +9,7 @@ function base(): FormState { return { providerID: "my-provider", name: "My Provider", + npm: "@ai-sdk/openai-compatible", baseURL: "https://example.com/v1", apiKey: "", models: [{ id: "model-1", name: "Model One", reasoning: false, variants: [] }], @@ -28,6 +29,13 @@ function args(form: FormState) { } describe("validateCustomProvider – variant name validation", () => { + it("persists the selected provider package", () => { + const form = base() + form.npm = "@ai-sdk/openai" + + expect(validateCustomProvider(args(form)).result?.config.npm).toBe("@ai-sdk/openai") + }) + it("allows reconnecting a disabled provider id", () => { const form = base() const out = validateCustomProvider({ @@ -56,6 +64,8 @@ describe("validateCustomProvider – variant name validation", () => { name: "fast", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, + outputEffort: undefined, reasoningEffort: undefined, chatTemplateArgs: undefined, }, @@ -73,6 +83,8 @@ describe("validateCustomProvider – variant name validation", () => { name: "", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, + outputEffort: undefined, reasoningEffort: undefined, chatTemplateArgs: undefined, }, @@ -90,6 +102,8 @@ describe("validateCustomProvider – variant name validation", () => { name: " ", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, + outputEffort: undefined, reasoningEffort: undefined, chatTemplateArgs: undefined, }, @@ -107,6 +121,8 @@ describe("validateCustomProvider – variant name validation", () => { name: "fast", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, + outputEffort: undefined, reasoningEffort: undefined, chatTemplateArgs: undefined, }, @@ -114,6 +130,8 @@ describe("validateCustomProvider – variant name validation", () => { name: "fast", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, + outputEffort: undefined, reasoningEffort: undefined, chatTemplateArgs: undefined, }, @@ -131,6 +149,8 @@ describe("validateCustomProvider – variant name validation", () => { name: "", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, + outputEffort: undefined, reasoningEffort: undefined, chatTemplateArgs: undefined, }, @@ -148,11 +168,27 @@ describe("validateCustomProvider – variant name validation", () => { const form = base() form.models[0].reasoning = true form.models[0].variants = [ - { name: "eco", enableThinking: true, thinking: undefined, reasoningEffort: "low", chatTemplateArgs: undefined }, + { + name: "eco", + enableThinking: true, + thinking: "adaptive", + splitReasoning: false, + outputEffort: "max", + reasoningEffort: "low", + chatTemplateArgs: undefined, + }, ] const out = validateCustomProvider(args(form)) expect(out.result).toBeDefined() const saved = out.result!.config.models["model-1"] as Record - expect(saved.variants).toEqual({ eco: { enable_thinking: true, reasoningEffort: "low" } }) + expect(saved.variants).toEqual({ + eco: { + enable_thinking: true, + thinking: { type: "adaptive" }, + reasoning_split: false, + effort: "max", + reasoningEffort: "low", + }, + }) }) }) diff --git a/packages/kilo-vscode/tests/unit/custom-provider.test.ts b/packages/kilo-vscode/tests/unit/custom-provider.test.ts index 4b7069bc527d..b91533769524 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider.test.ts @@ -8,6 +8,16 @@ import { validateProviderID, withCustomProviderDeletions, } from "../../src/shared/custom-provider" +import { isCustomProviderPackage } from "../../src/shared/provider-model" + +describe("isCustomProviderPackage", () => { + it("recognizes supported custom provider packages", () => { + expect(isCustomProviderPackage("@ai-sdk/openai-compatible")).toBe(true) + expect(isCustomProviderPackage("@ai-sdk/openai")).toBe(true) + expect(isCustomProviderPackage("@ai-sdk/anthropic")).toBe(true) + expect(isCustomProviderPackage("malicious-package")).toBe(false) + }) +}) describe("validateProviderID", () => { it("accepts valid provider ids", () => { @@ -64,9 +74,9 @@ describe("resolveCustomProviderKey", () => { }) describe("sanitizeCustomProviderConfig", () => { - it("normalizes config and forces the approved package", () => { + it("normalizes config and preserves an approved package", () => { const result = sanitizeCustomProviderConfig({ - npm: "malicious-package", + npm: "@ai-sdk/anthropic", name: " My Provider ", env: [" MY_PROVIDER_KEY "], options: { @@ -83,7 +93,7 @@ describe("sanitizeCustomProviderConfig", () => { expect(result).toEqual({ value: { - npm: "@ai-sdk/openai-compatible", + npm: "@ai-sdk/anthropic", name: "My Provider", env: ["MY_PROVIDER_KEY"], options: { @@ -100,7 +110,18 @@ describe("sanitizeCustomProviderConfig", () => { }) }) - it("accepts models with chat_template_args variant", () => { + it("rejects unapproved packages", () => { + const result = sanitizeCustomProviderConfig({ + npm: "malicious-package", + name: "Bad Provider", + options: { baseURL: "https://example.com/v1" }, + models: { "model-1": { name: "Model One" } }, + }) + + expect("error" in result ? result.error : "").toContain("Invalid enum value") + }) + + it("accepts supported thinking variant options", () => { const result = sanitizeCustomProviderConfig({ name: "Thinking Provider", options: { baseURL: "https://example.com/v1" }, @@ -108,7 +129,12 @@ describe("sanitizeCustomProviderConfig", () => { "model-1": { name: "Model One", variants: { - thinking: { chat_template_args: { enable_thinking: true } }, + thinking: { + thinking: { type: "adaptive" }, + reasoning_split: true, + effort: "max", + chat_template_args: { enable_thinking: true }, + }, }, }, }, @@ -123,7 +149,12 @@ describe("sanitizeCustomProviderConfig", () => { "model-1": { name: "Model One", variants: { - thinking: { chat_template_args: { enable_thinking: true } }, + thinking: { + thinking: { type: "adaptive" }, + reasoning_split: true, + effort: "max", + chat_template_args: { enable_thinking: true }, + }, }, }, }, @@ -190,6 +221,34 @@ describe("withCustomProviderDeletions", () => { expect(model.variants?.low).toBeNull() }) + it("emits null when reasoning is disabled on a surviving model", () => { + const existing = { models: { keep: { name: "Keep", reasoning: true } } } + const result = withCustomProviderDeletions(existing, baseNext) + expect(result.models.keep).toEqual({ name: "Keep", reasoning: null }) + }) + + it("emits null for options removed from a surviving variant", () => { + const existing = { + models: { + keep: { + name: "Keep", + variants: { + thinking: { thinking: { type: "adaptive" }, reasoning_split: true, reasoningEffort: "high" }, + }, + }, + }, + } + const next = { + ...baseNext, + models: { + keep: { name: "Keep", variants: { thinking: { reasoningEffort: "high" } } }, + }, + } as typeof baseNext + const result = withCustomProviderDeletions(existing, next) + const model = (result.models as Record }>).keep + expect(model.variants.thinking).toEqual({ reasoningEffort: "high", thinking: null, reasoning_split: null }) + }) + it("does not touch variants on a model that is being deleted", () => { const existing = { models: { gone: { name: "Gone", variants: { a: {} } } } } const result = withCustomProviderDeletions(existing, baseNext) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx index 9b24cacf22eb..c314e52269f8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx @@ -3,6 +3,7 @@ import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { Dialog } from "@kilocode/kilo-ui/dialog" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { ProviderIcon } from "@kilocode/kilo-ui/provider-icon" +import { Select } from "@kilocode/kilo-ui/select" import { Spinner } from "@kilocode/kilo-ui/spinner" import { TextField } from "@kilocode/kilo-ui/text-field" import { showToast } from "@kilocode/kilo-ui/toast" @@ -12,14 +13,20 @@ import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useProvider } from "../../context/provider" import { useVSCode } from "../../context/vscode" -import type { ExtensionMessage, ProviderConfig } from "../../types/messages" +import type { ExtensionMessage, ProviderAuthState, ProviderConfig } from "../../types/messages" import { createProviderAction } from "../../utils/provider-action" import { MASKED_CUSTOM_PROVIDER_KEY, resolveCustomProviderKey } from "../../../../src/shared/custom-provider" +import { + CUSTOM_PROVIDER_PACKAGE, + isCustomProviderPackage, + type CustomProviderPackage, +} from "../../../../src/shared/provider-model" import { ModelCard } from "./CustomProviderModelCard" import type { ChatTemplateArgsValue, EnableThinkingValue, ModelEntry, + OutputEffortValue, ReasoningEffortValue, ThinkingTypeValue, VariantEntry, @@ -29,6 +36,12 @@ import type { FormErrors, FormState, HeaderRow } from "./CustomProviderValidatio const DEBOUNCE_MS = 500 const SEARCH_DEBOUNCE_MS = 150 +const PACKAGE_OPTIONS: Array<{ value: CustomProviderPackage; label: string }> = [ + { value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" }, + { value: "@ai-sdk/openai", label: "OpenAI Responses" }, + { value: "@ai-sdk/anthropic", label: "Anthropic Messages" }, +] + /** Subsequence fuzzy match — "gpt4o" matches "gpt-4o-mini". */ function fuzzy(query: string, target: string) { const q = query.toLowerCase() @@ -41,15 +54,80 @@ function fuzzy(query: string, target: string) { } type FetchedModel = { id: string; name: string } +type RawModel = { name?: string; reasoning?: boolean; variants?: Record> } + +function parseVariant([name, cfg]: [string, Record]): VariantEntry { + return { + name, + enableThinking: typeof cfg.enable_thinking === "boolean" ? cfg.enable_thinking : undefined, + thinking: + typeof cfg.thinking === "object" && cfg.thinking !== null + ? ((cfg.thinking as { type?: string }).type as ThinkingTypeValue) + : undefined, + splitReasoning: typeof cfg.reasoning_split === "boolean" ? cfg.reasoning_split : undefined, + reasoningEffort: + typeof cfg.reasoningEffort === "string" ? (cfg.reasoningEffort as ReasoningEffortValue) : undefined, + outputEffort: typeof cfg.effort === "string" ? (cfg.effort as OutputEffortValue) : undefined, + chatTemplateArgs: + typeof cfg.chat_template_args === "object" && cfg.chat_template_args !== null + ? ((cfg.chat_template_args as { enable_thinking?: boolean }).enable_thinking as ChatTemplateArgsValue) + : undefined, + } +} + +function initModels(cfg: ProviderConfig | undefined): ModelEntry[] { + if (!cfg?.models || typeof cfg.models !== "object") return [{ id: "", name: "", reasoning: false, variants: [] }] + const entries = Object.entries(cfg.models) + if (entries.length === 0) return [{ id: "", name: "", reasoning: false, variants: [] }] + return entries.map(([id, model]) => { + const raw = model as RawModel + return { + id, + name: raw.name ?? id, + reasoning: raw.reasoning ?? false, + variants: Object.entries(raw.variants ?? {}).map(parseVariant), + } + }) +} + +function initHeaders(cfg: ProviderConfig | undefined): HeaderRow[] { + const opts = cfg?.options as { headers?: Record } | undefined + const headers = opts?.headers + if (!headers || typeof headers !== "object") return [{ key: "", value: "" }] + const entries = Object.entries(headers) + if (entries.length === 0) return [{ key: "", value: "" }] + return entries.map(([key, value]) => ({ key, value })) +} + +type ExistingProvider = { + providerID: string + name: string + config: ProviderConfig +} + +function resolveAuth(existing: ExistingProvider | undefined, states: Record) { + if (!existing || existing.config.env?.length) return + return states[existing.providerID] +} + +function initForm(existing: ExistingProvider | undefined, auth: ProviderAuthState | undefined): FormState { + const npm = existing?.config?.npm + return { + providerID: existing?.providerID ?? "", + name: existing?.name ?? "", + npm: isCustomProviderPackage(npm) ? npm : CUSTOM_PROVIDER_PACKAGE, + baseURL: (existing?.config?.options as { baseURL?: string } | undefined)?.baseURL ?? "", + apiKey: resolveCustomProviderKey(auth), + models: initModels(existing?.config), + headers: initHeaders(existing?.config), + saving: false, + } +} export interface CustomProviderDialogProps { onBack?: () => void /** When set, the dialog opens in edit mode with pre-filled values. */ - existing?: { - providerID: string - name: string - config: ProviderConfig - } + existing?: ExistingProvider } const CustomProviderDialog = (props: CustomProviderDialogProps) => { @@ -63,60 +141,8 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { const editing = () => !!props.existing - function initModels(): ModelEntry[] { - const cfg = props.existing?.config - if (!cfg?.models || typeof cfg.models !== "object") return [{ id: "", name: "", reasoning: false, variants: [] }] - const entries = Object.entries(cfg.models) - if (entries.length === 0) return [{ id: "", name: "", reasoning: false, variants: [] }] - return entries.map(([id, m]) => { - const raw = m as { name?: string; reasoning?: boolean; variants?: Record> } - const variants: VariantEntry[] = Object.entries(raw?.variants ?? {}).map(([vname, vcfg]) => ({ - name: vname, - enableThinking: typeof vcfg.enable_thinking === "boolean" ? (vcfg.enable_thinking as boolean) : undefined, - thinking: - typeof vcfg.thinking === "object" && vcfg.thinking !== null - ? ((vcfg.thinking as { type?: string }).type as ThinkingTypeValue) - : undefined, - reasoningEffort: - typeof vcfg.reasoningEffort === "string" ? (vcfg.reasoningEffort as ReasoningEffortValue) : undefined, - chatTemplateArgs: - typeof vcfg.chat_template_args === "object" && vcfg.chat_template_args !== null - ? ((vcfg.chat_template_args as { enable_thinking?: boolean }).enable_thinking as ChatTemplateArgsValue) - : undefined, - })) - return { - id, - name: raw?.name ?? id, - reasoning: raw?.reasoning ?? false, - variants, - } - }) - } - - function initHeaders(): HeaderRow[] { - const opts = props.existing?.config?.options as { headers?: Record } | undefined - const headers = opts?.headers - if (!headers || typeof headers !== "object") return [{ key: "", value: "" }] - const entries = Object.entries(headers) - if (entries.length === 0) return [{ key: "", value: "" }] - return entries.map(([key, value]) => ({ key, value })) - } - - const auth = props.existing?.config?.env?.length - ? undefined - : props.existing - ? provider.authStates()[props.existing.providerID] - : undefined - - const [form, setForm] = createStore({ - providerID: props.existing?.providerID ?? "", - name: props.existing?.name ?? "", - baseURL: (props.existing?.config?.options as { baseURL?: string } | undefined)?.baseURL ?? "", - apiKey: resolveCustomProviderKey(auth), - models: initModels(), - headers: initHeaders(), - saving: false, - }) + const auth = resolveAuth(props.existing, provider.authStates()) + const [form, setForm] = createStore(initForm(props.existing, auth)) const [errors, setErrors] = createStore({ providerID: undefined, @@ -160,11 +186,13 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { // SolidJS store proxies track at the property level — any store write // (including setForm("models", ...)) invalidates effects that read from // the same store, causing unwanted re-runs that wipe the model picker. + const [fetchPackage, setFetchPackage] = createSignal(form.npm) const [fetchURL, setFetchURL] = createSignal(form.baseURL) const [fetchKey, setFetchKey] = createSignal("") let fetchVersion = 0 createEffect(() => { + const npm = fetchPackage() const url = fetchURL() const key = fetchKey() void key // subscribe to key changes without using the value here @@ -175,7 +203,7 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { setFetchStatus(undefined) setSearch("") - if (!/^https?:\/\//.test(url.trim())) return + if (npm === "@ai-sdk/anthropic" || !/^https?:\/\//.test(url.trim())) return fetchVersion++ const version = fetchVersion @@ -354,7 +382,9 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { name: "", enableThinking: undefined, thinking: undefined, + splitReasoning: undefined, reasoningEffort: undefined, + outputEffort: undefined, chatTemplateArgs: undefined, } setForm("models", mi, "variants", (v) => [...v, blank]) @@ -490,6 +520,30 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { validationState={errors.name ? "invalid" : undefined} error={errors.name} /> +
+ + o.value === props.v.splitReasoning)} + value={(o) => String(o.value)} + label={(o) => props.t(o.labelKey)} + onSelect={(o) => props.onChangeSplitReasoning(o?.value)} + placeholder={props.t("provider.custom.models.variants.splitReasoning.placeholder")} + variant="secondary" + size="small" + triggerVariant="settings" + /> +
+
+ +