mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 14:43:37 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24c26dcf66 |
@@ -1,6 +1,7 @@
|
|||||||
import { ProviderID, type ModelID } from "../schema"
|
import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema"
|
||||||
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
|
import { Auth } from "../route/auth"
|
||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
import type { ProviderPackage } from "../provider-package"
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
||||||
@@ -19,6 +20,8 @@ export interface Settings extends ProviderPackage.Settings {
|
|||||||
readonly apiKey?: string
|
readonly apiKey?: string
|
||||||
readonly baseURL: string
|
readonly baseURL: string
|
||||||
readonly provider?: string
|
readonly provider?: string
|
||||||
|
readonly http?: RouteDefaultsInput["http"]
|
||||||
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||||
@@ -31,16 +34,24 @@ export const routes = [OpenAICompatibleChat.route]
|
|||||||
|
|
||||||
export const configure = (input: GenericModelOptions) => {
|
export const configure = (input: GenericModelOptions) => {
|
||||||
const provider = input.provider ?? "openai-compatible"
|
const provider = input.provider ?? "openai-compatible"
|
||||||
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input
|
const {
|
||||||
|
provider: _,
|
||||||
|
baseURL,
|
||||||
|
apiKey: _apiKey,
|
||||||
|
auth: _auth,
|
||||||
|
headers,
|
||||||
|
...rest
|
||||||
|
} = input
|
||||||
const route = OpenAICompatibleChat.route.with({
|
const route = OpenAICompatibleChat.route.with({
|
||||||
...rest,
|
...rest,
|
||||||
provider,
|
provider,
|
||||||
endpoint: { baseURL },
|
endpoint: { baseURL },
|
||||||
auth: AuthOptions.bearer(input, []),
|
auth: AuthOptions.bearer(input, []).andThen(Auth.headers(headers ?? {})),
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
id: ProviderID.make(provider),
|
id: ProviderID.make(provider),
|
||||||
model: (modelID: string | ModelID) =>
|
model: (modelID: string | ModelID) =>
|
||||||
|
// oxlint-disable-next-line typescript-eslint/no-unnecessary-type-arguments -- preserves provider-option validation at call sites
|
||||||
route.model<OpenAIProviderOptionsInput>({ id: modelID, provider: ProviderID.make(provider) }),
|
route.model<OpenAIProviderOptionsInput>({ id: modelID, provider: ProviderID.make(provider) }),
|
||||||
configure,
|
configure,
|
||||||
}
|
}
|
||||||
@@ -67,14 +78,18 @@ export const provider = {
|
|||||||
configure,
|
configure,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
|
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||||
configure({
|
configure({
|
||||||
apiKey: settings.apiKey,
|
apiKey: settings.apiKey,
|
||||||
baseURL: settings.baseURL,
|
baseURL: settings.baseURL,
|
||||||
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
http: mergeHttpOptions(
|
||||||
|
settings.http === undefined ? undefined : HttpOptions.make(settings.http),
|
||||||
|
settings.body === undefined ? undefined : new HttpOptions({ body: { ...settings.body } }),
|
||||||
|
),
|
||||||
limits: settings.limits,
|
limits: settings.limits,
|
||||||
provider: settings.provider,
|
provider: settings.provider,
|
||||||
|
providerOptions: settings.providerOptions,
|
||||||
}).model(modelID)
|
}).model(modelID)
|
||||||
|
|
||||||
export const baseten = define(profiles.baseten)
|
export const baseten = define(profiles.baseten)
|
||||||
|
|||||||
@@ -111,6 +111,22 @@ describe("provider package entrypoints", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("maps OpenAI-compatible Chat settings onto the executable model", async () => {
|
||||||
|
const OpenAICompatible = await import("@opencode-ai/ai/providers/openai-compatible")
|
||||||
|
const selected = OpenAICompatible.model("custom-model", {
|
||||||
|
apiKey: "fixture",
|
||||||
|
baseURL: "https://chat.example.test/v1",
|
||||||
|
provider: "example",
|
||||||
|
http: { query: { tenant: "one" } },
|
||||||
|
providerOptions: { openai: { reasoningEffort: "high" } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(String(selected.provider)).toBe("example")
|
||||||
|
expect(selected.route.id).toBe("openai-compatible-chat")
|
||||||
|
expect(selected.route.defaults.http?.query).toEqual({ tenant: "one" })
|
||||||
|
expect(selected.route.defaults.providerOptions).toEqual({ openai: { reasoningEffort: "high" } })
|
||||||
|
})
|
||||||
|
|
||||||
test("maps Anthropic-compatible settings onto the executable model", async () => {
|
test("maps Anthropic-compatible settings onto the executable model", async () => {
|
||||||
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
|
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
|
||||||
const selected = AnthropicCompatible.model("compatible-model", {
|
const selected = AnthropicCompatible.model("compatible-model", {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HttpClientRequest } from "effect/unstable/http"
|
|||||||
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src"
|
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src"
|
||||||
import { Auth, LLMClient } from "../../src/route"
|
import { Auth, LLMClient } from "../../src/route"
|
||||||
import { compileRequest } from "../../src/route/client"
|
import { compileRequest } from "../../src/route/client"
|
||||||
|
import { jsonRequestParts } from "../../src/route/transport/http"
|
||||||
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||||
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
||||||
import { it } from "../lib/effect"
|
import { it } from "../lib/effect"
|
||||||
@@ -144,6 +145,48 @@ describe("OpenAI-compatible Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("preserves compatible provider URL, usage, options, and body extensions", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const selected = OpenAICompatible.model("custom-model", {
|
||||||
|
apiKey: "generated-key",
|
||||||
|
baseURL: "https://compatible.example/v1",
|
||||||
|
provider: "custom",
|
||||||
|
headers: { Authorization: "Bearer configured-key" },
|
||||||
|
http: {
|
||||||
|
query: { tenant: "one" },
|
||||||
|
body: {
|
||||||
|
user: "user-1",
|
||||||
|
verbosity: "low",
|
||||||
|
vendor_extension: { enabled: true },
|
||||||
|
custom_boolean: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providerOptions: { openai: { reasoningEffort: "high" } },
|
||||||
|
})
|
||||||
|
const request = LLM.request({ model: selected, prompt: "Hello" })
|
||||||
|
const prepared = yield* compileRequest(request)
|
||||||
|
const parts = yield* jsonRequestParts({
|
||||||
|
endpoint: selected.route.endpoint,
|
||||||
|
auth: selected.route.auth,
|
||||||
|
headers: selected.route.headers,
|
||||||
|
request: LLMRequest.update(request, { http: selected.route.defaults.http }),
|
||||||
|
body: prepared.body,
|
||||||
|
encodeBody: (body) => JSON.stringify(body),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parts.url).toBe("https://compatible.example/v1/chat/completions?tenant=one")
|
||||||
|
expect(parts.headers.authorization).toBe("Bearer configured-key")
|
||||||
|
expect(parts.jsonBody).toMatchObject({
|
||||||
|
user: "user-1",
|
||||||
|
reasoning_effort: "high",
|
||||||
|
verbosity: "low",
|
||||||
|
vendor_extension: { enabled: true },
|
||||||
|
custom_boolean: false,
|
||||||
|
})
|
||||||
|
expect(parts.jsonBody).toMatchObject({ stream_options: { include_usage: true } })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("configures the max tokens request field", () =>
|
it.effect("configures the max tokens request field", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const compatible = OpenAICompatibleChat.route
|
const compatible = OpenAICompatibleChat.route
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export function map(input: MapInput): Mapping | undefined {
|
|||||||
...mapGoogleOptions(input.settings),
|
...mapGoogleOptions(input.settings),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
case "@ai-sdk/openai-compatible":
|
||||||
|
return mapOpenAICompatible(input.settings)
|
||||||
case "@openrouter/ai-sdk-provider":
|
case "@openrouter/ai-sdk-provider":
|
||||||
return mapOpenRouter(input.settings, baseSettings)
|
return mapOpenRouter(input.settings, baseSettings)
|
||||||
case "@ai-sdk/xai":
|
case "@ai-sdk/xai":
|
||||||
@@ -63,6 +65,34 @@ export function map(input: MapInput): Mapping | undefined {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOpenAICompatible(settings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||||
|
if (typeof settings.baseURL !== "string") return undefined
|
||||||
|
if (
|
||||||
|
settings.timeout !== undefined ||
|
||||||
|
settings.headerTimeout !== undefined ||
|
||||||
|
settings.chunkTimeout !== undefined ||
|
||||||
|
settings.fetch !== undefined ||
|
||||||
|
settings.transformRequestBody !== undefined ||
|
||||||
|
settings.metadataExtractor !== undefined ||
|
||||||
|
settings.supportsStructuredOutputs === true ||
|
||||||
|
settings.strictJsonSchema !== undefined
|
||||||
|
)
|
||||||
|
return undefined
|
||||||
|
const options = typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : undefined
|
||||||
|
return {
|
||||||
|
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||||
|
settings: {
|
||||||
|
baseURL: settings.baseURL,
|
||||||
|
...(typeof settings.name === "string" ? { provider: settings.name } : {}),
|
||||||
|
...mapAPIKey(settings),
|
||||||
|
...(isStringRecord(settings.queryParams) ? { http: { query: settings.queryParams } } : {}),
|
||||||
|
...(options === undefined ? {} : { providerOptions: { openai: options } }),
|
||||||
|
},
|
||||||
|
...(isStringRecord(settings.headers) ? { headers: settings.headers } : {}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||||
@@ -192,9 +222,7 @@ function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapBaseSettings(settings: Readonly<Record<string, unknown>>) {
|
function mapBaseSettings(settings: Readonly<Record<string, unknown>>) {
|
||||||
return {
|
return typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}
|
||||||
...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAPIKey(settings: Readonly<Record<string, unknown>>) {
|
function mapAPIKey(settings: Readonly<Record<string, unknown>>) {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { LanguageModel } from "@opencode-ai/ai"
|
|||||||
// ast-grep-ignore: no-star-import
|
// ast-grep-ignore: no-star-import
|
||||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||||
// ast-grep-ignore: no-star-import
|
// ast-grep-ignore: no-star-import
|
||||||
import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat"
|
|
||||||
// ast-grep-ignore: no-star-import
|
|
||||||
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
|
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
|
||||||
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
|
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
@@ -164,17 +162,6 @@ export const fromCatalogModel = (
|
|||||||
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
Provider.isAISDK(resolved.package) &&
|
|
||||||
packageName === "@ai-sdk/openai-compatible" &&
|
|
||||||
typeof resolved.settings?.baseURL === "string"
|
|
||||||
) {
|
|
||||||
return Effect.succeed(
|
|
||||||
withDefaults(resolved, OpenAICompatibleChat.route)
|
|
||||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
|
||||||
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const configured = { ...resolved.settings, ...credential?.metadata }
|
const configured = { ...resolved.settings, ...credential?.metadata }
|
||||||
const mapping = Provider.isAISDK(resolved.package)
|
const mapping = Provider.isAISDK(resolved.package)
|
||||||
? AISDKNative.map({
|
? AISDKNative.map({
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ function migrateStandardProvider(info: ConfigProviderV1.Info) {
|
|||||||
body: info.options && options.body,
|
body: info.options && options.body,
|
||||||
models:
|
models:
|
||||||
info.models &&
|
info.models &&
|
||||||
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])),
|
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,8 +294,9 @@ export function providerID(input: string) {
|
|||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
|
function migrateModel(info: typeof ConfigProviderV1.Model.Type, inheritedPackage?: string) {
|
||||||
const settings = info.options && ConfigProviderOptionsV1.model(info.options)
|
const packageName = info.provider?.npm ?? inheritedPackage
|
||||||
|
const overlays = info.options && ConfigProviderOptionsV1.modelOverlays(info.options, packageName)
|
||||||
const costs = info.cost && [
|
const costs = info.cost && [
|
||||||
{
|
{
|
||||||
input: info.cost.input,
|
input: info.cost.input,
|
||||||
@@ -323,14 +324,15 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
|
|||||||
name: info.name,
|
name: info.name,
|
||||||
compatibility: Model.compatibility(info.interleaved),
|
compatibility: Model.compatibility(info.interleaved),
|
||||||
package: info.provider?.npm ? Provider.aisdk(info.provider.npm) : undefined,
|
package: info.provider?.npm ? Provider.aisdk(info.provider.npm) : undefined,
|
||||||
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
|
settings: info.provider?.api ? { ...overlays?.settings, baseURL: info.provider.api } : overlays?.settings,
|
||||||
|
body: overlays?.body,
|
||||||
capabilities,
|
capabilities,
|
||||||
headers: info.headers,
|
headers: info.headers,
|
||||||
variants:
|
variants:
|
||||||
info.variants &&
|
info.variants &&
|
||||||
Object.entries(info.variants).map(([id, options]) => ({
|
Object.entries(info.variants).map(([id, options]) => ({
|
||||||
id,
|
id,
|
||||||
settings: ConfigProviderOptionsV1.model(options),
|
...ConfigProviderOptionsV1.modelOverlays(options, packageName),
|
||||||
})),
|
})),
|
||||||
cost: costs,
|
cost: costs,
|
||||||
disabled: info.status === "deprecated" ? true : undefined,
|
disabled: info.status === "deprecated" ? true : undefined,
|
||||||
|
|||||||
@@ -29,3 +29,18 @@ export function provider(options: Options): ProviderResult {
|
|||||||
export function model(options: Options) {
|
export function model(options: Options) {
|
||||||
return { ...options }
|
return { ...options }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function modelOverlays(options: Options, packageName: string | undefined) {
|
||||||
|
if (packageName !== "@ai-sdk/openai-compatible") return { settings: model(options) }
|
||||||
|
const known = new Set(["reasoningEffort", "strictJsonSchema"])
|
||||||
|
const settings = Object.fromEntries(Object.entries(options).filter(([key]) => known.has(key)))
|
||||||
|
const body = Object.fromEntries(
|
||||||
|
Object.entries(options)
|
||||||
|
.filter(([key]) => !known.has(key))
|
||||||
|
.map(([key, value]) => [key === "textVerbosity" ? "verbosity" : key, value]),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
settings: Object.keys(settings).length === 0 ? undefined : settings,
|
||||||
|
body: Object.keys(body).length === 0 ? undefined : body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,49 @@ const map = (packageName: string, settings: Readonly<Record<string, unknown>>, m
|
|||||||
AISDKNative.map({ packageName, settings, modelID })
|
AISDKNative.map({ packageName, settings, modelID })
|
||||||
|
|
||||||
describe("AISDKNative", () => {
|
describe("AISDKNative", () => {
|
||||||
|
test("maps the generic OpenAI-compatible package to the native provider package", () => {
|
||||||
|
expect(
|
||||||
|
map("@ai-sdk/openai-compatible", {
|
||||||
|
apiKey: "secret",
|
||||||
|
baseURL: "https://compatible.example/v1",
|
||||||
|
name: "example",
|
||||||
|
headers: { "x-test": "value" },
|
||||||
|
queryParams: { tenant: "one" },
|
||||||
|
reasoningEffort: "high",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||||
|
settings: {
|
||||||
|
apiKey: "secret",
|
||||||
|
baseURL: "https://compatible.example/v1",
|
||||||
|
provider: "example",
|
||||||
|
http: { query: { tenant: "one" } },
|
||||||
|
providerOptions: {
|
||||||
|
openai: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: { "x-test": "value" },
|
||||||
|
})
|
||||||
|
expect(map("@ai-sdk/openai-compatible", {})).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
map("@ai-sdk/openai-compatible", { baseURL: "https://compatible.example/v1", timeout: 30_000 }),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
map("@ai-sdk/openai-compatible", {
|
||||||
|
baseURL: "https://compatible.example/v1",
|
||||||
|
supportsStructuredOutputs: true,
|
||||||
|
}),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
map("@ai-sdk/openai-compatible", {
|
||||||
|
baseURL: "https://compatible.example/v1",
|
||||||
|
strictJsonSchema: false,
|
||||||
|
}),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test("maps both models.dev Bedrock packages to native providers", () => {
|
test("maps both models.dev Bedrock packages to native providers", () => {
|
||||||
expect(map("@ai-sdk/amazon-bedrock", { region: "us-east-1" })).toEqual({
|
expect(map("@ai-sdk/amazon-bedrock", { region: "us-east-1" })).toEqual({
|
||||||
package: "@opencode-ai/ai/providers/amazon-bedrock",
|
package: "@opencode-ai/ai/providers/amazon-bedrock",
|
||||||
|
|||||||
@@ -570,6 +570,72 @@ describe("Config", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("preserves serializable OpenAI-compatible options across v1 migration", () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const migrated = ConfigMigrateV1.migrate({
|
||||||
|
provider: {
|
||||||
|
acme: {
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
api: "https://api.example/v1",
|
||||||
|
options: {
|
||||||
|
apiKey: "secret",
|
||||||
|
name: "acme",
|
||||||
|
headers: { "x-provider": "yes" },
|
||||||
|
body: { provider_body_extension: true },
|
||||||
|
queryParams: { tenant: "one" },
|
||||||
|
includeUsage: false,
|
||||||
|
supportsStructuredOutputs: true,
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
chat: {
|
||||||
|
options: {
|
||||||
|
user: "user-1",
|
||||||
|
reasoningEffort: "high",
|
||||||
|
textVerbosity: "low",
|
||||||
|
strictJsonSchema: false,
|
||||||
|
vendor_extension: { enabled: true },
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
strict: { strictJsonSchema: true, variant_extension: "value" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(migrated.providers?.acme).toMatchObject({
|
||||||
|
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||||
|
settings: {
|
||||||
|
apiKey: "secret",
|
||||||
|
name: "acme",
|
||||||
|
queryParams: { tenant: "one" },
|
||||||
|
includeUsage: false,
|
||||||
|
supportsStructuredOutputs: true,
|
||||||
|
baseURL: "https://api.example/v1",
|
||||||
|
},
|
||||||
|
headers: { "x-provider": "yes" },
|
||||||
|
body: { provider_body_extension: true },
|
||||||
|
models: {
|
||||||
|
chat: {
|
||||||
|
settings: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
strictJsonSchema: false,
|
||||||
|
},
|
||||||
|
body: { user: "user-1", verbosity: "low", vendor_extension: { enabled: true } },
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "strict",
|
||||||
|
settings: { strictJsonSchema: true },
|
||||||
|
body: { variant_extension: "value" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("renames old provider IDs while migrating v1 configuration", () =>
|
it.effect("renames old provider IDs while migrating v1 configuration", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const migrated = ConfigMigrateV1.migrate({
|
const migrated = ConfigMigrateV1.migrate({
|
||||||
|
|||||||
@@ -38,6 +38,28 @@ describe("ConfigProviderOptionsV1", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("splits OpenAI-compatible model options into native settings and body extensions", () => {
|
||||||
|
expect(
|
||||||
|
ConfigProviderOptionsV1.modelOverlays(
|
||||||
|
{
|
||||||
|
user: "user-1",
|
||||||
|
reasoningEffort: "high",
|
||||||
|
textVerbosity: "low",
|
||||||
|
strictJsonSchema: false,
|
||||||
|
vendor_extension: { enabled: true },
|
||||||
|
store: false,
|
||||||
|
},
|
||||||
|
"@ai-sdk/openai-compatible",
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
settings: {
|
||||||
|
reasoningEffort: "high",
|
||||||
|
strictJsonSchema: false,
|
||||||
|
},
|
||||||
|
body: { user: "user-1", verbosity: "low", vendor_extension: { enabled: true }, store: false },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("uses mechanical lowering for custom provider options", () => {
|
test("uses mechanical lowering for custom provider options", () => {
|
||||||
expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
|
expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
|
||||||
settings: { enabled: true },
|
settings: { enabled: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user