1b5ef0737f
* chore: details * fix(tui): add visual feedback for diff wrap and conceal toggles (#10655) * fix: await SessionRevert.cleanup for shell (#10669) * fix: Make diff wrapping toggle always available in command_list and correct a type error (resolves #10682) (#10683) * feat(app): default servers on web * fix(app): handle non-tool call permissions * fix(app): line selection waits on ready * wip(app): model settings * feat(app): model settings * chore: generate * wip(app): sidebar hover full * wip(app): full-height sidebar * wip(app): full-height sidebar * feat(app): better sidebar hover when collapsed * fix(app): query selector with non-latin chars * chore: stale-while-revalidate headers for changelog * chore: generate * feat(opencode): add agent description (#10680) * feat: dynamically resolve AGENTS.md files from subdirectories as agent explores them (#10678) * chore: gen changelog page off changelog json * upgrade opentui to v0.1.75 * chore: update nix node_modules hashes * fix: move changelog footer outside content div to fix padding (#10712) * fix(app): Order themes alphabetically (#10698) * fix(app): restore external link opening in system browser (#10697) * fix(ui): use focus-visible instead of focus to prevent sticky hover effect on click (#10651) * fix: add 44px top padding to sticky version header on changelog (#10715) * feat(app): file tree * wip(app): file tree mode * wip(app): file tree mode * chore: cleanup * chore: cleanup * chore(app): createStore over signals * fix(enterprise): add message navigation to share page desktop view (#10071) Co-authored-by: Claude <noreply@anthropic.com> * chore: fix changelog page * Revert "fix(app): restore external link opening in system browser (#10697)" This reverts commit984518b1c0. * ignore: tweak ai deps * feat: use anthropic compat messages api for anthropic models through copilot * chore: debug changelog * fix: ensure openai 404 errors are retried (#10590) * chore: generate * test(app): e2e test for sidebar nav * chore: refactor changelog * fix(app): session load cap * chore(app): missing i18n strings * chore: cleanup * fix(app): session sync issue * chore: generate * fix(ui): update button styles and disconnect button size * fix(app): set provider row height to 56px * fix(app): add hover text for env-connected providers * fix(app): use default cursor for env provider text * feat(ui): add providers icon and use in settings * feat(ui): add models icon and use in settings * fix(app): style view all button with interactive color and margin * feat(ui): add optional transition animations to dialog * fix(ui): prevent double-close and fix dialog replacement * feat(app): add transition to select provider dialog * feat(app): add transition to command palette * fix(ui): reduce dialog transition in time to 150ms * feat(app): add provider descriptions to settings * feat(app): add Vercel AI Gateway provider description * fix(ui): scope filetree pill tabs styling * fix(app): adjust filetree panel padding * fix(app): tighten filetree row spacing * fix(app): use chevron icons for filetree folders * fix(app): use medium font for filetree items * fix(app): refine filetree row spacing and indent * fix(app): reduce filetree folder indent * fix(app): dim non-deep filetree guide lines * fix(app): fade filetree guide lines on hover * fix(app): move file tree toggle to right * fix(app): disable tooltips in filetree tabs * test(app): fix e2e test * ci: add stale pr job * ci: add dry-run option to stale PR closer workflow Allows testing stale PR closure without actually closing PRs * tweak: retry logic to catch certain provider problems * fix(app): disconnect zen provider * fix(app): zen disconnect not working * chore: generate * chore: cleanup * fix: search clear icon * wip: new release modal - highlight key updates or new features - needs some transition love - all copy including text and video placeholder * chore: cleanup * wip: highlights * wip: highlights * wip: highlights * tweak: when using messages api for copilot, attach anthropic beta headers * chore: don't flip github draft release automatically * fix(app): file tree performance * fix(app): reactive file tree * chore: generate * chore: cleanup * fix(app): enable file watcher * chore: cleanup * chore: cleanup * test(app): file tree spec * feat(release): add highlights template to draft releases (#10745) * docs: add Italian README (#10732) * chore: generate * fix(app): add connect provier in model selector (#10706) * fix(desktop): disable magnification gestures on macOS (#10605) * feat: add /learn command to extract session learnings to scoped AGENTS.md files (#10717) * fix: use Instance.directory instead of process.cwd() in read tool * fix(core): expose Instance.directory to custom tools * fix(app): only show files in select dialog when clicking + tab * desktop: deduplicate tauri configs * ignore: update download stats 2026-01-27 * fix(app): auto-scroll button sometimes sticks * chore(app): missing tooltips * feat(app): forward and back buttons * chore: cleanup * feat(core): add worktree to plugin tool context * fix(app): agent fallback colors * fix(app): non-fatal error handling * docs(core): plugin tool context updates * fix(ui): set filetree tablist height to 48px with centered content * fix(app): center filetree empty state with 32px top margin * fix(app): update review empty states to 14px and align select file empty state * fix(app): only show left border on plus button when sticky * fix(app): use smaller close icon on tabs to match comment cards * fix(ui): add max-width 280px to tabs with text truncation * fix(app): don't show session skeleton after workspace reset * perf(app): don't keep parts in memory * perf(app): cleanup connect provider timers * perf(app): shared terminal ghostty-web instance * fix(app): select model anchor * Revert "fix(app): select model anchor" This reverts commit 36babf5fc390fd9a892ad3742690740296d94a23. * fix(app): open markdown links in external browser * fix(app): navigate to tabs when opening file * chore(app): cleanup tailwind vs pure css * fix(app): tooltips causing getComputedStyle errors in model select * fix(app): swallow file search errors * feat(opencode): Handle Venice cache creation tokens (#10735) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> * chore: generate * chore(docs): Better explanation on how to allow tools in external directories (#10862) * tweak: adjust retry check to be more defensive * ci: make tests passing a requirement pre-release * zen: kimi k2.5 and minimax m2.1 * feat(app): update settings in general settings * chore(app): translations * test(app): new e2e smoke tests * feat(app): show loaded agents.md files * fix(app): session diffs not always loading * feat(app): improved layout * chore: generate * tweak: add recommended topP/temp for kimi k2.5 * Set temperature for kimi k2.5 * Revert "ci: make tests passing a requirement pre-release" This reverts commit8c00818108. * Revert "Set temperature for kimi k2.5" This reverts commitbb63d16fa8. * feat(app): color filetree change dots by diff kind * feat(app): add filetree tooltips with diff labels * fix(app): shorten nav tooltips * fix(app): adjust titlebar left spacing * fix(app): delay nav tooltips * fix(app): align filetree change styling * fix(app): set filetree padding to 6px * fix(app): highlight selected change Track clicked file in the Changes tree and apply selection styling to the matching review diff. * chore(app): show 5 highlights * fix(app): auto-scroll * fix(app): don't connect to localhost through vpn * perf(app): better memory management * chore: cleanup * feat(desktop): i18n for tauri side * feat(desktop): tauri locales * fix(app): not auto-navigating to last project * chore: update nix node_modules hashes * fix(tui): handle 4-5 codes too in c to copy logic * fix(app): spinner color * fix(app): file tree not always loading * test(app): fix outdated e2e test * test(app): fix outdated e2e test * fix(app): file tree not always loading * fix(app): terminal corruption * feat(app): add 'connect provider' button to the manage models dialog (#10887) * fix(app): auto-scroll * feat: support headless authentication for chatgpt/codex (#10890) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> * fix(app): reintroduce review tab * release: v1.1.37 * feat(app): use opentui markdown component behind experimental flag (#10900) * chore: generate * docs: add Daytona OpenCode plugin to ecosystem (#10917) * chore: generate * fix(cli): restore brand integrity of CLI wordmark (#10912) * zen: kimi k2.5 * fix(opencode): ensure unsub(PartUpdated) is always called in TaskTool (#9992) * chore: generate * ignore: adjust flag * bump plugin version * ci * fix: ensure parallel tool calls dont double load AGENTS.md * release: v1.1.38 * fix: ensure copilot plugin properly sets headers for new messages api * chore: generate * release: v1.1.39 * bump: plugins * ignore: update download stats 2026-01-28 * chore(app): global config changes * feat(app): custom provider * chore: cleanup * wip(app): global config * chore: cleanup * chore: cleanup * chore: generate * feat(i18n): add th locale support (#10809) * chore: generate * fix(desktop): enable ctrl+n and ctrl+p for popover navigation (#10777) * fix: add SubtaskPart with metadata reference (#10990) Co-authored-by: yangtianzhe <yangtianzhe@corp.netease.com> * chore: generate * fix(app): close review pane * fix(markdown): Add streaming prop to markdown element (#11025) * release: v1.1.40 * fix(script): remove highlights template from release notes (#11052) * chore: generate * fix(ui): allow KaTeX inline math to be followed by punctuation (#11033) * fix(app): alignment and padding in dialogs (#10866) * fix(app): fill missing zh keys to avoid English fallback (#10841) * chore: generate * fix(opencode): normalize zh punctuation for Chinese UI (#10842) * fix(ui): improve zh duration display formatting (#10844) * fix(opencode): add `AbortSignal` support to `Ripgrep.files()` and `GlobTool` (#10833) * fix(typescript errors): remove duplicate keys causing typescript failures (#11071) * fix(app): file tabs - auto scroll on open & scroll via mouse wheel (#11070) * tweak: add ctx.abort to grep tool * fix(app): types * docs: update experimental environment variables in CLI docs (#11030) * fix(app): make settings more responsive for mobile and small web/desktop (#10775) * chore: generate * fix(opencode): allow media-src data: URL for small audio files (#11082) * fix(provider): include providerID in SDK cache key (#11020) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * fix: maxOutputTokens was accidentally hardcoded to undefined (#10995) * fix: ensure variants for copilot models work w/ maxTokens being set * release: v1.1.41 * test: fix test * chore: generate * feat(cli): include cache tokens in stats (#10582) * ci: adjust team * guard destroyed input field in timeout * feat: Sequential numbering for forked session titles (Issue #10105) (#10321) * more timeout race guards * fix: ensure that kimi doesnt have fake variants available * fix: frontmatter was adding newlines in some cases causing invalid model ids (#11095) Co-authored-by: aptdnfapt <aptdnfapt@users.noreply.github.com> * fix: undo change that used anthropic messages endpoint for anthropic models on copilot due to ratelimiting issues, go back to completions endpoint instead * release: v1.1.42 * fix: fix types * fix: fix kilocode/sdk type * chore: update nix node_modules hashes --------- Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> Co-authored-by: Idris Gadi <85882535+IdrisGit@users.noreply.github.com> Co-authored-by: Noam Bressler <noamzbr@gmail.com> Co-authored-by: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Steffen Deusch <steffen@deusch.me> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Sebastian Herrlinger <hasta84@gmail.com> Co-authored-by: Ryan Vogel <ryan@mandarin3d.com> Co-authored-by: Sam Huckaby <samhuckaby@gmail.com> Co-authored-by: Alex Yaroshuk <34632190+alexyaroshuk@users.noreply.github.com> Co-authored-by: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: zerone0x <hi@trine.dev> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: Tim <hello@timsmart.co> Co-authored-by: David Hill <iamdavidhill@gmail.com> Co-authored-by: Fabio Martino <fabiomartino@users.noreply.github.com> Co-authored-by: Chris Yang <18487241+ysm-dev@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: dpuyosa <dpuyosa@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Rohan Godha <git@rohangodha.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Tommy D. Rossi <beats.by.morse@gmail.com> Co-authored-by: James Murdza <james@jamesmurdza.com> Co-authored-by: Tito <github@mynameistito.com> Co-authored-by: Goni Zahavy <goni1993@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Nattawee Phantawong <crozzio@gmail.com> Co-authored-by: Sairaj <sairaj2119@gmail.com> Co-authored-by: RenegadeE <37529469+YangTianz@users.noreply.github.com> Co-authored-by: yangtianzhe <yangtianzhe@corp.netease.com> Co-authored-by: Max Kong <kongzheyuan@outlook.com> Co-authored-by: Julian Coy <julian@juliancoy.com> Co-authored-by: Babou <42550761+bbabou@users.noreply.github.com> Co-authored-by: Devin Griffin <31415269+DNGriffin@users.noreply.github.com> Co-authored-by: tan <zerotanapok@gmail.com> Co-authored-by: Saba Tchikhinashvili <38084512+saba-ch@users.noreply.github.com> Co-authored-by: ideallove <20013535+ideallove@users.noreply.github.com> Co-authored-by: Benjamin Oldenburg <benjamin.oldenburg@ordis.co.th> Co-authored-by: aptdnfapt <aptdnfapt@users.noreply.github.com> Co-authored-by: marius-kilocode <marius@kilocode.ai>
1728 lines
50 KiB
TypeScript
1728 lines
50 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { ProviderTransform } from "../../src/provider/transform"
|
|
|
|
const OUTPUT_TOKEN_MAX = 32000
|
|
|
|
describe("ProviderTransform.options - setCacheKey", () => {
|
|
const sessionID = "test-session-123"
|
|
|
|
const mockModel = {
|
|
id: "anthropic/claude-3-5-sonnet",
|
|
providerID: "anthropic",
|
|
api: {
|
|
id: "claude-3-5-sonnet-20241022",
|
|
url: "https://api.anthropic.com",
|
|
npm: "@ai-sdk/anthropic",
|
|
},
|
|
name: "Claude 3.5 Sonnet",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: false,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: {
|
|
input: 0.003,
|
|
output: 0.015,
|
|
cache: { read: 0.0003, write: 0.00375 },
|
|
},
|
|
limit: {
|
|
context: 200000,
|
|
output: 8192,
|
|
},
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
} as any
|
|
|
|
test("should set promptCacheKey when providerOptions.setCacheKey is true", () => {
|
|
const result = ProviderTransform.options({
|
|
model: mockModel,
|
|
sessionID,
|
|
providerOptions: { setCacheKey: true },
|
|
})
|
|
expect(result.promptCacheKey).toBe(sessionID)
|
|
})
|
|
|
|
test("should not set promptCacheKey when providerOptions.setCacheKey is false", () => {
|
|
const result = ProviderTransform.options({
|
|
model: mockModel,
|
|
sessionID,
|
|
providerOptions: { setCacheKey: false },
|
|
})
|
|
expect(result.promptCacheKey).toBeUndefined()
|
|
})
|
|
|
|
test("should not set promptCacheKey when providerOptions is undefined", () => {
|
|
const result = ProviderTransform.options({
|
|
model: mockModel,
|
|
sessionID,
|
|
providerOptions: undefined,
|
|
})
|
|
expect(result.promptCacheKey).toBeUndefined()
|
|
})
|
|
|
|
test("should not set promptCacheKey when providerOptions does not have setCacheKey", () => {
|
|
const result = ProviderTransform.options({ model: mockModel, sessionID, providerOptions: {} })
|
|
expect(result.promptCacheKey).toBeUndefined()
|
|
})
|
|
|
|
test("should set promptCacheKey for openai provider regardless of setCacheKey", () => {
|
|
const openaiModel = {
|
|
...mockModel,
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-4",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
}
|
|
const result = ProviderTransform.options({ model: openaiModel, sessionID, providerOptions: {} })
|
|
expect(result.promptCacheKey).toBe(sessionID)
|
|
})
|
|
|
|
test("should set store=false for openai provider", () => {
|
|
const openaiModel = {
|
|
...mockModel,
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-4",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
}
|
|
const result = ProviderTransform.options({
|
|
model: openaiModel,
|
|
sessionID,
|
|
providerOptions: {},
|
|
})
|
|
expect(result.store).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.maxOutputTokens", () => {
|
|
test("returns 32k when modelLimit > 32k", () => {
|
|
const modelLimit = 100000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/openai", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
})
|
|
|
|
test("returns modelLimit when modelLimit < 32k", () => {
|
|
const modelLimit = 16000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/openai", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(16000)
|
|
})
|
|
|
|
describe("azure", () => {
|
|
test("returns 32k when modelLimit > 32k", () => {
|
|
const modelLimit = 100000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/azure", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
})
|
|
|
|
test("returns modelLimit when modelLimit < 32k", () => {
|
|
const modelLimit = 16000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/azure", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(16000)
|
|
})
|
|
})
|
|
|
|
describe("bedrock", () => {
|
|
test("returns 32k when modelLimit > 32k", () => {
|
|
const modelLimit = 100000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/amazon-bedrock", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
})
|
|
|
|
test("returns modelLimit when modelLimit < 32k", () => {
|
|
const modelLimit = 16000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/amazon-bedrock", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(16000)
|
|
})
|
|
})
|
|
|
|
describe("anthropic without thinking options", () => {
|
|
test("returns 32k when modelLimit > 32k", () => {
|
|
const modelLimit = 100000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
})
|
|
|
|
test("returns modelLimit when modelLimit < 32k", () => {
|
|
const modelLimit = 16000
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", {}, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(16000)
|
|
})
|
|
})
|
|
|
|
describe("anthropic with thinking options", () => {
|
|
test("returns 32k when budgetTokens + 32k <= modelLimit", () => {
|
|
const modelLimit = 100000
|
|
const options = {
|
|
thinking: {
|
|
type: "enabled",
|
|
budgetTokens: 10000,
|
|
},
|
|
}
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
})
|
|
|
|
test("returns modelLimit - budgetTokens when budgetTokens + 32k > modelLimit", () => {
|
|
const modelLimit = 50000
|
|
const options = {
|
|
thinking: {
|
|
type: "enabled",
|
|
budgetTokens: 30000,
|
|
},
|
|
}
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(20000)
|
|
})
|
|
|
|
test("returns 32k when thinking type is not enabled", () => {
|
|
const modelLimit = 100000
|
|
const options = {
|
|
thinking: {
|
|
type: "disabled",
|
|
budgetTokens: 10000,
|
|
},
|
|
}
|
|
const result = ProviderTransform.maxOutputTokens("@ai-sdk/anthropic", options, modelLimit, OUTPUT_TOKEN_MAX)
|
|
expect(result).toBe(OUTPUT_TOKEN_MAX)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.schema - gemini array items", () => {
|
|
test("adds missing items for array properties", () => {
|
|
const geminiModel = {
|
|
providerID: "google",
|
|
api: {
|
|
id: "gemini-3-pro",
|
|
},
|
|
} as any
|
|
|
|
const schema = {
|
|
type: "object",
|
|
properties: {
|
|
nodes: { type: "array" },
|
|
edges: { type: "array", items: { type: "string" } },
|
|
},
|
|
} as any
|
|
|
|
const result = ProviderTransform.schema(geminiModel, schema) as any
|
|
|
|
expect(result.properties.nodes.items).toBeDefined()
|
|
expect(result.properties.edges.items.type).toBe("string")
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "reasoning", text: "Let me think about this..." },
|
|
{
|
|
type: "tool-call",
|
|
toolCallId: "test",
|
|
toolName: "bash",
|
|
input: { command: "echo hello" },
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(
|
|
msgs,
|
|
{
|
|
id: "deepseek/deepseek-chat",
|
|
providerID: "deepseek",
|
|
api: {
|
|
id: "deepseek-chat",
|
|
url: "https://api.deepseek.com",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
name: "DeepSeek Chat",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: true,
|
|
attachment: false,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: {
|
|
field: "reasoning_content",
|
|
},
|
|
},
|
|
cost: {
|
|
input: 0.001,
|
|
output: 0.002,
|
|
cache: { read: 0.0001, write: 0.0002 },
|
|
},
|
|
limit: {
|
|
context: 128000,
|
|
output: 8192,
|
|
},
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
release_date: "2023-04-01",
|
|
},
|
|
{},
|
|
)
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toEqual([
|
|
{
|
|
type: "tool-call",
|
|
toolCallId: "test",
|
|
toolName: "bash",
|
|
input: { command: "echo hello" },
|
|
},
|
|
])
|
|
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
|
|
})
|
|
|
|
test("Non-DeepSeek providers leave reasoning content unchanged", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "reasoning", text: "Should not be processed" },
|
|
{ type: "text", text: "Answer" },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(
|
|
msgs,
|
|
{
|
|
id: "openai/gpt-4",
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-4",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
name: "GPT-4",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: false,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: {
|
|
input: 0.03,
|
|
output: 0.06,
|
|
cache: { read: 0.001, write: 0.002 },
|
|
},
|
|
limit: {
|
|
context: 128000,
|
|
output: 4096,
|
|
},
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
release_date: "2023-04-01",
|
|
},
|
|
{},
|
|
)
|
|
|
|
expect(result[0].content).toEqual([
|
|
{ type: "reasoning", text: "Should not be processed" },
|
|
{ type: "text", text: "Answer" },
|
|
])
|
|
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.message - empty image handling", () => {
|
|
const mockModel = {
|
|
id: "anthropic/claude-3-5-sonnet",
|
|
providerID: "anthropic",
|
|
api: {
|
|
id: "claude-3-5-sonnet-20241022",
|
|
url: "https://api.anthropic.com",
|
|
npm: "@ai-sdk/anthropic",
|
|
},
|
|
name: "Claude 3.5 Sonnet",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: false,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: {
|
|
input: 0.003,
|
|
output: 0.015,
|
|
cache: { read: 0.0003, write: 0.00375 },
|
|
},
|
|
limit: {
|
|
context: 200000,
|
|
output: 8192,
|
|
},
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
} as any
|
|
|
|
test("should replace empty base64 image with error text", () => {
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "What is in this image?" },
|
|
{ type: "image", image: "data:image/png;base64," },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, mockModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(2)
|
|
expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
|
|
expect(result[0].content[1]).toEqual({
|
|
type: "text",
|
|
text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
|
|
})
|
|
})
|
|
|
|
test("should keep valid base64 images unchanged", () => {
|
|
const validBase64 =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "What is in this image?" },
|
|
{ type: "image", image: `data:image/png;base64,${validBase64}` },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, mockModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(2)
|
|
expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
|
|
expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
|
|
})
|
|
|
|
test("should handle mixed valid and empty images", () => {
|
|
const validBase64 =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "Compare these images" },
|
|
{ type: "image", image: `data:image/png;base64,${validBase64}` },
|
|
{ type: "image", image: "data:image/jpeg;base64," },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, mockModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(3)
|
|
expect(result[0].content[0]).toEqual({ type: "text", text: "Compare these images" })
|
|
expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
|
|
expect(result[0].content[2]).toEqual({
|
|
type: "text",
|
|
text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.message - anthropic empty content filtering", () => {
|
|
const anthropicModel = {
|
|
id: "anthropic/claude-3-5-sonnet",
|
|
providerID: "anthropic",
|
|
api: {
|
|
id: "claude-3-5-sonnet-20241022",
|
|
url: "https://api.anthropic.com",
|
|
npm: "@ai-sdk/anthropic",
|
|
},
|
|
name: "Claude 3.5 Sonnet",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: false,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: {
|
|
input: 0.003,
|
|
output: 0.015,
|
|
cache: { read: 0.0003, write: 0.00375 },
|
|
},
|
|
limit: {
|
|
context: 200000,
|
|
output: 8192,
|
|
},
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
} as any
|
|
|
|
test("filters out messages with empty string content", () => {
|
|
const msgs = [
|
|
{ role: "user", content: "Hello" },
|
|
{ role: "assistant", content: "" },
|
|
{ role: "user", content: "World" },
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {})
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].content).toBe("Hello")
|
|
expect(result[1].content).toBe("World")
|
|
})
|
|
|
|
test("filters out empty text parts from array content", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "" },
|
|
{ type: "text", text: "Hello" },
|
|
{ type: "text", text: "" },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(1)
|
|
expect(result[0].content[0]).toEqual({ type: "text", text: "Hello" })
|
|
})
|
|
|
|
test("filters out empty reasoning parts from array content", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "reasoning", text: "" },
|
|
{ type: "text", text: "Answer" },
|
|
{ type: "reasoning", text: "" },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(1)
|
|
expect(result[0].content[0]).toEqual({ type: "text", text: "Answer" })
|
|
})
|
|
|
|
test("removes entire message when all parts are empty", () => {
|
|
const msgs = [
|
|
{ role: "user", content: "Hello" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "" },
|
|
{ type: "reasoning", text: "" },
|
|
],
|
|
},
|
|
{ role: "user", content: "World" },
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {})
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].content).toBe("Hello")
|
|
expect(result[1].content).toBe("World")
|
|
})
|
|
|
|
test("keeps non-text/reasoning parts even if text parts are empty", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "" },
|
|
{ type: "tool-call", toolCallId: "123", toolName: "bash", input: { command: "ls" } },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(1)
|
|
expect(result[0].content[0]).toEqual({
|
|
type: "tool-call",
|
|
toolCallId: "123",
|
|
toolName: "bash",
|
|
input: { command: "ls" },
|
|
})
|
|
})
|
|
|
|
test("keeps messages with valid text alongside empty parts", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "reasoning", text: "Thinking..." },
|
|
{ type: "text", text: "" },
|
|
{ type: "text", text: "Result" },
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {})
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content).toHaveLength(2)
|
|
expect(result[0].content[0]).toEqual({ type: "reasoning", text: "Thinking..." })
|
|
expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
|
|
})
|
|
|
|
test("does not filter for non-anthropic providers", () => {
|
|
const openaiModel = {
|
|
...anthropicModel,
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-4",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
}
|
|
|
|
const msgs = [
|
|
{ role: "assistant", content: "" },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "" }],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, openaiModel, {})
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].content).toBe("")
|
|
expect(result[1].content).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.message - strip openai metadata when store=false", () => {
|
|
const openaiModel = {
|
|
id: "openai/gpt-5",
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-5",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
name: "GPT-5",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: true,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
|
|
limit: { context: 128000, output: 4096 },
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
} as any
|
|
|
|
test("preserves itemId and reasoningEncryptedContent when store=false", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "reasoning",
|
|
text: "thinking...",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "rs_123",
|
|
reasoningEncryptedContent: "encrypted",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "msg_456",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
|
|
expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
|
|
})
|
|
|
|
test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => {
|
|
const zenModel = {
|
|
...openaiModel,
|
|
providerID: "zen",
|
|
}
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "reasoning",
|
|
text: "thinking...",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "rs_123",
|
|
reasoningEncryptedContent: "encrypted",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "msg_456",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
|
|
expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
|
|
})
|
|
|
|
test("preserves other openai options including itemId", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "msg_123",
|
|
otherOption: "value",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
|
|
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
|
|
expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value")
|
|
})
|
|
|
|
test("preserves metadata for openai package when store is true", () => {
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "msg_123",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
// openai package preserves itemId regardless of store value
|
|
const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
|
|
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
|
|
})
|
|
|
|
test("preserves metadata for non-openai packages when store is false", () => {
|
|
const anthropicModel = {
|
|
...openaiModel,
|
|
providerID: "anthropic",
|
|
api: {
|
|
id: "claude-3",
|
|
url: "https://api.anthropic.com",
|
|
npm: "@ai-sdk/anthropic",
|
|
},
|
|
}
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "msg_123",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
// store=false preserves metadata for non-openai packages
|
|
const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
|
|
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
|
|
})
|
|
|
|
test("preserves metadata using providerID key when store is false", () => {
|
|
const opencodeModel = {
|
|
...openaiModel,
|
|
providerID: "opencode",
|
|
api: {
|
|
id: "opencode-test",
|
|
url: "https://api.opencode.ai",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
}
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
opencode: {
|
|
itemId: "msg_123",
|
|
otherOption: "value",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
|
|
|
|
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123")
|
|
expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
|
|
})
|
|
|
|
test("preserves itemId across all providerOptions keys", () => {
|
|
const opencodeModel = {
|
|
...openaiModel,
|
|
providerID: "opencode",
|
|
api: {
|
|
id: "opencode-test",
|
|
url: "https://api.opencode.ai",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
}
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
providerOptions: {
|
|
openai: { itemId: "msg_root" },
|
|
opencode: { itemId: "msg_opencode" },
|
|
extra: { itemId: "msg_extra" },
|
|
},
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: { itemId: "msg_openai_part" },
|
|
opencode: { itemId: "msg_opencode_part" },
|
|
extra: { itemId: "msg_extra_part" },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
|
|
|
|
expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
|
|
expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
|
|
expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
|
|
expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
|
|
expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part")
|
|
})
|
|
|
|
test("does not strip metadata for non-openai packages when store is not false", () => {
|
|
const anthropicModel = {
|
|
...openaiModel,
|
|
providerID: "anthropic",
|
|
api: {
|
|
id: "claude-3",
|
|
url: "https://api.anthropic.com",
|
|
npm: "@ai-sdk/anthropic",
|
|
},
|
|
}
|
|
const msgs = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
providerOptions: {
|
|
openai: {
|
|
itemId: "msg_123",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
|
|
|
|
expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.message - providerOptions key remapping", () => {
|
|
const createModel = (providerID: string, npm: string) =>
|
|
({
|
|
id: `${providerID}/test-model`,
|
|
providerID,
|
|
api: {
|
|
id: "test-model",
|
|
url: "https://api.test.com",
|
|
npm,
|
|
},
|
|
name: "Test Model",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: false,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
|
|
limit: { context: 128000, output: 8192 },
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
}) as any
|
|
|
|
test("azure keeps 'azure' key and does not remap to 'openai'", () => {
|
|
const model = createModel("azure", "@ai-sdk/azure")
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: "Hello",
|
|
providerOptions: {
|
|
azure: { someOption: "value" },
|
|
},
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, model, {})
|
|
|
|
expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
|
|
expect(result[0].providerOptions?.openai).toBeUndefined()
|
|
})
|
|
|
|
test("openai with github-copilot npm remaps providerID to 'openai'", () => {
|
|
const model = createModel("github-copilot", "@ai-sdk/github-copilot")
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: "Hello",
|
|
providerOptions: {
|
|
"github-copilot": { someOption: "value" },
|
|
},
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, model, {})
|
|
|
|
expect(result[0].providerOptions?.openai).toEqual({ someOption: "value" })
|
|
expect(result[0].providerOptions?.["github-copilot"]).toBeUndefined()
|
|
})
|
|
|
|
test("bedrock remaps providerID to 'bedrock' key", () => {
|
|
const model = createModel("my-bedrock", "@ai-sdk/amazon-bedrock")
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: "Hello",
|
|
providerOptions: {
|
|
"my-bedrock": { someOption: "value" },
|
|
},
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, model, {})
|
|
|
|
expect(result[0].providerOptions?.bedrock).toEqual({ someOption: "value" })
|
|
expect(result[0].providerOptions?.["my-bedrock"]).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.message - claude w/bedrock custom inference profile", () => {
|
|
test("adds cachePoint", () => {
|
|
const model = {
|
|
id: "amazon-bedrock/custom-claude-sonnet-4.5",
|
|
providerID: "amazon-bedrock",
|
|
api: {
|
|
id: "arn:aws:bedrock:xxx:yyy:application-inference-profile/zzz",
|
|
url: "https://api.test.com",
|
|
npm: "@ai-sdk/amazon-bedrock",
|
|
},
|
|
name: "Custom inference profile",
|
|
capabilities: {},
|
|
options: {},
|
|
headers: {},
|
|
} as any
|
|
|
|
const msgs = [
|
|
{
|
|
role: "user",
|
|
content: "Hello",
|
|
},
|
|
] as any[]
|
|
|
|
const result = ProviderTransform.message(msgs, model, {})
|
|
|
|
expect(result[0].providerOptions?.bedrock).toEqual(
|
|
expect.objectContaining({
|
|
cachePoint: {
|
|
type: "ephemeral",
|
|
},
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("ProviderTransform.variants", () => {
|
|
const createMockModel = (overrides: Partial<any> = {}): any => ({
|
|
id: "test/test-model",
|
|
providerID: "test",
|
|
api: {
|
|
id: "test-model",
|
|
url: "https://api.test.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
name: "Test Model",
|
|
capabilities: {
|
|
temperature: true,
|
|
reasoning: true,
|
|
attachment: true,
|
|
toolcall: true,
|
|
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
interleaved: false,
|
|
},
|
|
cost: {
|
|
input: 0.001,
|
|
output: 0.002,
|
|
cache: { read: 0.0001, write: 0.0002 },
|
|
},
|
|
limit: {
|
|
context: 200_000,
|
|
output: 64_000,
|
|
},
|
|
status: "active",
|
|
options: {},
|
|
headers: {},
|
|
release_date: "2024-01-01",
|
|
...overrides,
|
|
})
|
|
|
|
test("returns empty object when model has no reasoning capabilities", () => {
|
|
const model = createMockModel({
|
|
capabilities: { reasoning: false },
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("deepseek returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "deepseek/deepseek-chat",
|
|
providerID: "deepseek",
|
|
api: {
|
|
id: "deepseek-chat",
|
|
url: "https://api.deepseek.com",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("minimax returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "minimax/minimax-model",
|
|
providerID: "minimax",
|
|
api: {
|
|
id: "minimax-model",
|
|
url: "https://api.minimax.com",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("glm returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "glm/glm-4",
|
|
providerID: "glm",
|
|
api: {
|
|
id: "glm-4",
|
|
url: "https://api.glm.com",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("mistral returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "mistral/mistral-large",
|
|
providerID: "mistral",
|
|
api: {
|
|
id: "mistral-large-latest",
|
|
url: "https://api.mistral.com",
|
|
npm: "@ai-sdk/mistral",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
describe("@openrouter/ai-sdk-provider", () => {
|
|
test("returns empty object for non-qualifying models", () => {
|
|
const model = createMockModel({
|
|
id: "openrouter/test-model",
|
|
providerID: "openrouter",
|
|
api: {
|
|
id: "test-model",
|
|
url: "https://openrouter.ai",
|
|
npm: "@openrouter/ai-sdk-provider",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("gpt models return OPENAI_EFFORTS with reasoning", () => {
|
|
const model = createMockModel({
|
|
id: "openrouter/gpt-4",
|
|
providerID: "openrouter",
|
|
api: {
|
|
id: "gpt-4",
|
|
url: "https://openrouter.ai",
|
|
npm: "@openrouter/ai-sdk-provider",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
|
|
expect(result.low).toEqual({ reasoning: { effort: "low" } })
|
|
expect(result.high).toEqual({ reasoning: { effort: "high" } })
|
|
})
|
|
|
|
test("gemini-3 returns OPENAI_EFFORTS with reasoning", () => {
|
|
const model = createMockModel({
|
|
id: "openrouter/gemini-3-5-pro",
|
|
providerID: "openrouter",
|
|
api: {
|
|
id: "gemini-3-5-pro",
|
|
url: "https://openrouter.ai",
|
|
npm: "@openrouter/ai-sdk-provider",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
|
|
})
|
|
|
|
test("grok-4 returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "openrouter/grok-4",
|
|
providerID: "openrouter",
|
|
api: {
|
|
id: "grok-4",
|
|
url: "https://openrouter.ai",
|
|
npm: "@openrouter/ai-sdk-provider",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("grok-3-mini returns low and high with reasoning", () => {
|
|
const model = createMockModel({
|
|
id: "openrouter/grok-3-mini",
|
|
providerID: "openrouter",
|
|
api: {
|
|
id: "grok-3-mini",
|
|
url: "https://openrouter.ai",
|
|
npm: "@openrouter/ai-sdk-provider",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "high"])
|
|
expect(result.low).toEqual({ reasoning: { effort: "low" } })
|
|
expect(result.high).toEqual({ reasoning: { effort: "high" } })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/gateway", () => {
|
|
test("returns OPENAI_EFFORTS with reasoningEffort", () => {
|
|
const model = createMockModel({
|
|
id: "gateway/gateway-model",
|
|
providerID: "gateway",
|
|
api: {
|
|
id: "gateway-model",
|
|
url: "https://gateway.ai",
|
|
npm: "@ai-sdk/gateway",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
|
|
expect(result.low).toEqual({ reasoningEffort: "low" })
|
|
expect(result.high).toEqual({ reasoningEffort: "high" })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/github-copilot", () => {
|
|
test("standard models return low, medium, high", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-4.5",
|
|
providerID: "github-copilot",
|
|
api: {
|
|
id: "gpt-4.5",
|
|
url: "https://api.githubcopilot.com",
|
|
npm: "@ai-sdk/github-copilot",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({
|
|
reasoningEffort: "low",
|
|
reasoningSummary: "auto",
|
|
include: ["reasoning.encrypted_content"],
|
|
})
|
|
})
|
|
|
|
test("gpt-5.1-codex-max includes xhigh", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5.1-codex-max",
|
|
providerID: "github-copilot",
|
|
api: {
|
|
id: "gpt-5.1-codex-max",
|
|
url: "https://api.githubcopilot.com",
|
|
npm: "@ai-sdk/github-copilot",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
|
|
})
|
|
|
|
test("gpt-5.1-codex-mini does not include xhigh", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5.1-codex-mini",
|
|
providerID: "github-copilot",
|
|
api: {
|
|
id: "gpt-5.1-codex-mini",
|
|
url: "https://api.githubcopilot.com",
|
|
npm: "@ai-sdk/github-copilot",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
})
|
|
|
|
test("gpt-5.1-codex does not include xhigh", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5.1-codex",
|
|
providerID: "github-copilot",
|
|
api: {
|
|
id: "gpt-5.1-codex",
|
|
url: "https://api.githubcopilot.com",
|
|
npm: "@ai-sdk/github-copilot",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
})
|
|
|
|
test("gpt-5.2 includes xhigh", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5.2",
|
|
providerID: "github-copilot",
|
|
api: {
|
|
id: "gpt-5.2",
|
|
url: "https://api.githubcopilot.com",
|
|
npm: "@ai-sdk/github-copilot",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
|
|
expect(result.xhigh).toEqual({
|
|
reasoningEffort: "xhigh",
|
|
reasoningSummary: "auto",
|
|
include: ["reasoning.encrypted_content"],
|
|
})
|
|
})
|
|
|
|
test("gpt-5.2-codex includes xhigh", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5.2-codex",
|
|
providerID: "github-copilot",
|
|
api: {
|
|
id: "gpt-5.2-codex",
|
|
url: "https://api.githubcopilot.com",
|
|
npm: "@ai-sdk/github-copilot",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/cerebras", () => {
|
|
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
|
|
const model = createMockModel({
|
|
id: "cerebras/llama-4",
|
|
providerID: "cerebras",
|
|
api: {
|
|
id: "llama-4-sc",
|
|
url: "https://api.cerebras.ai",
|
|
npm: "@ai-sdk/cerebras",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({ reasoningEffort: "low" })
|
|
expect(result.high).toEqual({ reasoningEffort: "high" })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/togetherai", () => {
|
|
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
|
|
const model = createMockModel({
|
|
id: "togetherai/llama-4",
|
|
providerID: "togetherai",
|
|
api: {
|
|
id: "llama-4-sc",
|
|
url: "https://api.togetherai.com",
|
|
npm: "@ai-sdk/togetherai",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({ reasoningEffort: "low" })
|
|
expect(result.high).toEqual({ reasoningEffort: "high" })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/xai", () => {
|
|
test("grok-3 returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "xai/grok-3",
|
|
providerID: "xai",
|
|
api: {
|
|
id: "grok-3",
|
|
url: "https://api.x.ai",
|
|
npm: "@ai-sdk/xai",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("grok-3-mini returns low and high with reasoningEffort", () => {
|
|
const model = createMockModel({
|
|
id: "xai/grok-3-mini",
|
|
providerID: "xai",
|
|
api: {
|
|
id: "grok-3-mini",
|
|
url: "https://api.x.ai",
|
|
npm: "@ai-sdk/xai",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "high"])
|
|
expect(result.low).toEqual({ reasoningEffort: "low" })
|
|
expect(result.high).toEqual({ reasoningEffort: "high" })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/deepinfra", () => {
|
|
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
|
|
const model = createMockModel({
|
|
id: "deepinfra/llama-4",
|
|
providerID: "deepinfra",
|
|
api: {
|
|
id: "llama-4-sc",
|
|
url: "https://api.deepinfra.com",
|
|
npm: "@ai-sdk/deepinfra",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({ reasoningEffort: "low" })
|
|
expect(result.high).toEqual({ reasoningEffort: "high" })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/openai-compatible", () => {
|
|
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
|
|
const model = createMockModel({
|
|
id: "custom-provider/custom-model",
|
|
providerID: "custom-provider",
|
|
api: {
|
|
id: "custom-model",
|
|
url: "https://api.custom.com",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({ reasoningEffort: "low" })
|
|
expect(result.high).toEqual({ reasoningEffort: "high" })
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/azure", () => {
|
|
test("o1-mini returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "o1-mini",
|
|
providerID: "azure",
|
|
api: {
|
|
id: "o1-mini",
|
|
url: "https://azure.com",
|
|
npm: "@ai-sdk/azure",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("standard azure models return custom efforts with reasoningSummary", () => {
|
|
const model = createMockModel({
|
|
id: "o1",
|
|
providerID: "azure",
|
|
api: {
|
|
id: "o1",
|
|
url: "https://azure.com",
|
|
npm: "@ai-sdk/azure",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({
|
|
reasoningEffort: "low",
|
|
reasoningSummary: "auto",
|
|
include: ["reasoning.encrypted_content"],
|
|
})
|
|
})
|
|
|
|
test("gpt-5 adds minimal effort", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5",
|
|
providerID: "azure",
|
|
api: {
|
|
id: "gpt-5",
|
|
url: "https://azure.com",
|
|
npm: "@ai-sdk/azure",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/openai", () => {
|
|
test("gpt-5-pro returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5-pro",
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-5-pro",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
test("standard openai models return custom efforts with reasoningSummary", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5",
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-5",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
release_date: "2024-06-01",
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
|
|
expect(result.low).toEqual({
|
|
reasoningEffort: "low",
|
|
reasoningSummary: "auto",
|
|
include: ["reasoning.encrypted_content"],
|
|
})
|
|
})
|
|
|
|
test("models after 2025-11-13 include 'none' effort", () => {
|
|
const model = createMockModel({
|
|
id: "gpt-5-nano",
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-5-nano",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
release_date: "2025-11-14",
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high"])
|
|
})
|
|
|
|
test("models after 2025-12-04 include 'xhigh' effort", () => {
|
|
const model = createMockModel({
|
|
id: "openai/gpt-5-chat",
|
|
providerID: "openai",
|
|
api: {
|
|
id: "gpt-5-chat",
|
|
url: "https://api.openai.com",
|
|
npm: "@ai-sdk/openai",
|
|
},
|
|
release_date: "2025-12-05",
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/anthropic", () => {
|
|
test("returns high and max with thinking config", () => {
|
|
const model = createMockModel({
|
|
id: "anthropic/claude-4",
|
|
providerID: "anthropic",
|
|
api: {
|
|
id: "claude-4",
|
|
url: "https://api.anthropic.com",
|
|
npm: "@ai-sdk/anthropic",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["high", "max"])
|
|
expect(result.high).toEqual({
|
|
thinking: {
|
|
type: "enabled",
|
|
budgetTokens: 16000,
|
|
},
|
|
})
|
|
expect(result.max).toEqual({
|
|
thinking: {
|
|
type: "enabled",
|
|
budgetTokens: 31999,
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/amazon-bedrock", () => {
|
|
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
|
|
const model = createMockModel({
|
|
id: "bedrock/llama-4",
|
|
providerID: "bedrock",
|
|
api: {
|
|
id: "llama-4-sc",
|
|
url: "https://bedrock.amazonaws.com",
|
|
npm: "@ai-sdk/amazon-bedrock",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
|
expect(result.low).toEqual({
|
|
reasoningConfig: {
|
|
type: "enabled",
|
|
maxReasoningEffort: "low",
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/google", () => {
|
|
test("gemini-2.5 returns high and max with thinkingConfig and thinkingBudget", () => {
|
|
const model = createMockModel({
|
|
id: "google/gemini-2.5-pro",
|
|
providerID: "google",
|
|
api: {
|
|
id: "gemini-2.5-pro",
|
|
url: "https://generativelanguage.googleapis.com",
|
|
npm: "@ai-sdk/google",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["high", "max"])
|
|
expect(result.high).toEqual({
|
|
thinkingConfig: {
|
|
includeThoughts: true,
|
|
thinkingBudget: 16000,
|
|
},
|
|
})
|
|
expect(result.max).toEqual({
|
|
thinkingConfig: {
|
|
includeThoughts: true,
|
|
thinkingBudget: 24576,
|
|
},
|
|
})
|
|
})
|
|
|
|
test("other gemini models return low and high with thinkingLevel", () => {
|
|
const model = createMockModel({
|
|
id: "google/gemini-2.0-pro",
|
|
providerID: "google",
|
|
api: {
|
|
id: "gemini-2.0-pro",
|
|
url: "https://generativelanguage.googleapis.com",
|
|
npm: "@ai-sdk/google",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "high"])
|
|
expect(result.low).toEqual({
|
|
includeThoughts: true,
|
|
thinkingLevel: "low",
|
|
})
|
|
expect(result.high).toEqual({
|
|
includeThoughts: true,
|
|
thinkingLevel: "high",
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/google-vertex", () => {
|
|
test("gemini-2.5 returns high and max with thinkingConfig and thinkingBudget", () => {
|
|
const model = createMockModel({
|
|
id: "google-vertex/gemini-2.5-pro",
|
|
providerID: "google-vertex",
|
|
api: {
|
|
id: "gemini-2.5-pro",
|
|
url: "https://vertexai.googleapis.com",
|
|
npm: "@ai-sdk/google-vertex",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["high", "max"])
|
|
})
|
|
|
|
test("other vertex models return low and high with thinkingLevel", () => {
|
|
const model = createMockModel({
|
|
id: "google-vertex/gemini-2.0-pro",
|
|
providerID: "google-vertex",
|
|
api: {
|
|
id: "gemini-2.0-pro",
|
|
url: "https://vertexai.googleapis.com",
|
|
npm: "@ai-sdk/google-vertex",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["low", "high"])
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/cohere", () => {
|
|
test("returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "cohere/command-r",
|
|
providerID: "cohere",
|
|
api: {
|
|
id: "command-r",
|
|
url: "https://api.cohere.com",
|
|
npm: "@ai-sdk/cohere",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/groq", () => {
|
|
test("returns none and WIDELY_SUPPORTED_EFFORTS with thinkingLevel", () => {
|
|
const model = createMockModel({
|
|
id: "groq/llama-4",
|
|
providerID: "groq",
|
|
api: {
|
|
id: "llama-4-sc",
|
|
url: "https://api.groq.com",
|
|
npm: "@ai-sdk/groq",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(Object.keys(result)).toEqual(["none", "low", "medium", "high"])
|
|
expect(result.none).toEqual({
|
|
includeThoughts: true,
|
|
thinkingLevel: "none",
|
|
})
|
|
expect(result.low).toEqual({
|
|
includeThoughts: true,
|
|
thinkingLevel: "low",
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("@ai-sdk/perplexity", () => {
|
|
test("returns empty object", () => {
|
|
const model = createMockModel({
|
|
id: "perplexity/sonar-plus",
|
|
providerID: "perplexity",
|
|
api: {
|
|
id: "sonar-plus",
|
|
url: "https://api.perplexity.ai",
|
|
npm: "@ai-sdk/perplexity",
|
|
},
|
|
})
|
|
const result = ProviderTransform.variants(model)
|
|
expect(result).toEqual({})
|
|
})
|
|
})
|
|
})
|