models-dev.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import path from "path"
  2. import { describe, expect } from "bun:test"
  3. import { Money } from "@opencode-ai/schema/money"
  4. import { Effect, Layer } from "effect"
  5. import { Catalog } from "@opencode-ai/core/catalog"
  6. import { Integration } from "@opencode-ai/core/integration"
  7. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  8. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  9. import { Bus } from "@opencode-ai/core/bus"
  10. import { Location } from "@opencode-ai/core/location"
  11. import { Model } from "@opencode-ai/core/model"
  12. import { ModelsDev } from "@opencode-ai/core/models-dev"
  13. import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
  14. import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
  15. import { Provider } from "@opencode-ai/core/provider"
  16. import { AbsolutePath } from "@opencode-ai/core/schema"
  17. import { location } from "../fixture/location"
  18. import { testEffect } from "../lib/effect"
  19. import { catalogHost, host, integrationHost } from "./host"
  20. const locationLayer = Layer.succeed(
  21. Location.Service,
  22. Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
  23. )
  24. const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, Bus.node]), [
  25. [Location.node, locationLayer],
  26. ])
  27. const it = testEffect(layer)
  28. const models = (file: string) =>
  29. AppNodeBuilder.build(ModelsDev.node, [[ModelsDev.node, ModelsDev.configured({ file, fetch: false })]])
  30. describe("ModelsDevPlugin", () => {
  31. it.effect("projects normalized models.dev snapshots into the catalog", () =>
  32. Effect.gen(function* () {
  33. const integrations = yield* Integration.Service
  34. const catalog = yield* Catalog.Service
  35. const providerID = Provider.ID.make("acme")
  36. const modelID = Model.ID.make("gpt-5.4")
  37. const models = ModelsDev.Service.of({
  38. get: () =>
  39. Effect.succeed([
  40. {
  41. info: {
  42. id: providerID,
  43. name: "Acme",
  44. package: Provider.aisdk("@ai-sdk/openai-compatible"),
  45. settings: { baseURL: "https://api.acme.test/v1" },
  46. },
  47. environment: [],
  48. models: [
  49. {
  50. id: modelID,
  51. modelID,
  52. providerID,
  53. name: "GPT-5.4",
  54. family: Model.Family.make("gpt"),
  55. capabilities: { tools: true, input: [], output: [] },
  56. variants: [],
  57. time: { released: Date.parse("2026-01-01") },
  58. cost: [
  59. {
  60. input: Money.USDPerMillionTokens.make(2.5),
  61. output: Money.USDPerMillionTokens.make(15),
  62. cache: {
  63. read: Money.USDPerMillionTokens.zero,
  64. write: Money.USDPerMillionTokens.zero,
  65. },
  66. },
  67. {
  68. tier: { type: "context", size: 272_000 },
  69. input: Money.USDPerMillionTokens.make(3),
  70. output: Money.USDPerMillionTokens.make(18),
  71. cache: {
  72. read: Money.USDPerMillionTokens.make(0.25),
  73. write: Money.USDPerMillionTokens.zero,
  74. },
  75. },
  76. {
  77. tier: { type: "context", size: 200_000 },
  78. input: Money.USDPerMillionTokens.make(5),
  79. output: Money.USDPerMillionTokens.make(22.5),
  80. cache: {
  81. read: Money.USDPerMillionTokens.make(0.5),
  82. write: Money.USDPerMillionTokens.zero,
  83. },
  84. },
  85. ],
  86. status: "active",
  87. enabled: true,
  88. limit: { context: 1_050_000, input: 922_000, output: 128_000 },
  89. },
  90. {
  91. id: Model.ID.make("gpt-5.4-fast"),
  92. modelID,
  93. providerID,
  94. name: "GPT-5.4 Fast",
  95. family: Model.Family.make("gpt"),
  96. package: Provider.aisdk("@ai-sdk/openai-compatible"),
  97. settings: { baseURL: "https://api.acme.test/v1" },
  98. headers: { "x-mode": "fast" },
  99. body: { service_tier: "priority" },
  100. capabilities: { tools: true, input: [], output: [] },
  101. variants: [],
  102. time: { released: Date.parse("2026-01-01") },
  103. cost: [
  104. {
  105. input: Money.USDPerMillionTokens.make(5),
  106. output: Money.USDPerMillionTokens.make(30),
  107. cache: {
  108. read: Money.USDPerMillionTokens.make(0.5),
  109. write: Money.USDPerMillionTokens.zero,
  110. },
  111. },
  112. {
  113. tier: { type: "context", size: 272_000 },
  114. input: Money.USDPerMillionTokens.make(3),
  115. output: Money.USDPerMillionTokens.make(18),
  116. cache: {
  117. read: Money.USDPerMillionTokens.make(0.25),
  118. write: Money.USDPerMillionTokens.zero,
  119. },
  120. },
  121. {
  122. tier: { type: "context", size: 200_000 },
  123. input: Money.USDPerMillionTokens.make(5),
  124. output: Money.USDPerMillionTokens.make(22.5),
  125. cache: {
  126. read: Money.USDPerMillionTokens.make(0.5),
  127. write: Money.USDPerMillionTokens.zero,
  128. },
  129. },
  130. ],
  131. status: "active",
  132. enabled: true,
  133. limit: { context: 1_050_000, input: 922_000, output: 128_000 },
  134. },
  135. ],
  136. },
  137. ] satisfies readonly ModelsDev.Snapshot[]),
  138. refresh: () => Effect.void,
  139. })
  140. yield* ModelsDevPlugin.effect(
  141. host({
  142. catalog: catalogHost(catalog),
  143. integration: integrationHost(integrations),
  144. }),
  145. ).pipe(Effect.provideService(ModelsDev.Service, models))
  146. const base = yield* catalog.model.get(providerID, Model.ID.make("gpt-5.4"))
  147. const fast = yield* catalog.model.get(providerID, Model.ID.make("gpt-5.4-fast"))
  148. expect(base?.variants).toEqual([])
  149. expect(base?.body).toBeUndefined()
  150. expect(fast).toMatchObject({
  151. id: "gpt-5.4-fast",
  152. modelID: "gpt-5.4",
  153. providerID: "acme",
  154. name: "GPT-5.4 Fast",
  155. package: Provider.aisdk("@ai-sdk/openai-compatible"),
  156. settings: { baseURL: "https://api.acme.test/v1" },
  157. headers: { "x-mode": "fast" },
  158. body: { service_tier: "priority" },
  159. variants: [],
  160. })
  161. expect(fast?.cost).toEqual([
  162. {
  163. input: Money.USDPerMillionTokens.make(5),
  164. output: Money.USDPerMillionTokens.make(30),
  165. cache: {
  166. read: Money.USDPerMillionTokens.make(0.5),
  167. write: Money.USDPerMillionTokens.zero,
  168. },
  169. },
  170. {
  171. tier: { type: "context", size: 272_000 },
  172. input: Money.USDPerMillionTokens.make(3),
  173. output: Money.USDPerMillionTokens.make(18),
  174. cache: {
  175. read: Money.USDPerMillionTokens.make(0.25),
  176. write: Money.USDPerMillionTokens.zero,
  177. },
  178. },
  179. {
  180. tier: { type: "context", size: 200_000 },
  181. input: Money.USDPerMillionTokens.make(5),
  182. output: Money.USDPerMillionTokens.make(22.5),
  183. cache: {
  184. read: Money.USDPerMillionTokens.make(0.5),
  185. write: Money.USDPerMillionTokens.zero,
  186. },
  187. },
  188. ])
  189. }),
  190. )
  191. it.effect("registers key methods for providers with environment variables", () =>
  192. Effect.gen(function* () {
  193. const integrations = yield* Integration.Service
  194. const catalog = yield* Catalog.Service
  195. yield* ModelsDevPlugin.effect(
  196. host({
  197. catalog: catalogHost(catalog),
  198. integration: integrationHost(integrations),
  199. }),
  200. )
  201. expect(yield* integrations.list()).toEqual([
  202. Integration.Info.make({
  203. id: Integration.ID.make("acme"),
  204. name: "Acme",
  205. methods: [
  206. { type: "key" },
  207. {
  208. type: "env",
  209. names: ["ACME_API_KEY"],
  210. },
  211. ],
  212. connections: [],
  213. }),
  214. ])
  215. }).pipe(Effect.provide(models(path.join(import.meta.dir, "fixtures", "models-dev.json")))),
  216. )
  217. it.effect("omits legacy provider aliases", () =>
  218. Effect.gen(function* () {
  219. const integrations = yield* Integration.Service
  220. const catalog = yield* Catalog.Service
  221. const snapshots = [
  222. ["azure", "Azure", "AZURE_API_KEY", "@ai-sdk/azure"],
  223. ["azure-cognitive-services", "Azure Cognitive Services", "AZURE_COGNITIVE_SERVICES_API_KEY", "@ai-sdk/azure"],
  224. ["google-vertex", "Google Vertex", "GOOGLE_APPLICATION_CREDENTIALS", "@ai-sdk/google-vertex"],
  225. [
  226. "google-vertex-anthropic",
  227. "Google Vertex Anthropic",
  228. "GOOGLE_APPLICATION_CREDENTIALS",
  229. "@ai-sdk/google-vertex/anthropic",
  230. ],
  231. ].map(([id, name, environment, packageName]) => ({
  232. info: {
  233. id: Provider.ID.make(id),
  234. name,
  235. package: Provider.aisdk(packageName),
  236. },
  237. environment: id === "azure" ? ["AZURE_RESOURCE_NAME", environment] : [environment],
  238. models: [],
  239. })) satisfies readonly ModelsDev.Snapshot[]
  240. yield* ModelsDevPlugin.effect(
  241. host({
  242. catalog: catalogHost(catalog),
  243. integration: integrationHost(integrations),
  244. }),
  245. ).pipe(
  246. Effect.provideService(
  247. ModelsDev.Service,
  248. ModelsDev.Service.of({
  249. get: () => Effect.succeed(snapshots),
  250. refresh: () => Effect.void,
  251. }),
  252. ),
  253. )
  254. expect(yield* catalog.provider.get(Provider.ID.azure)).toBeDefined()
  255. expect(yield* catalog.provider.get(Provider.ID.make("google-vertex"))).toBeDefined()
  256. expect(yield* catalog.provider.get(Provider.ID.make("azure-cognitive-services"))).toBeUndefined()
  257. expect(yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic"))).toBeUndefined()
  258. expect(yield* integrations.get(Integration.ID.make("azure"))).toBeDefined()
  259. expect(yield* integrations.get(Integration.ID.make("azure"))).toMatchObject({
  260. methods: [{ type: "key" }, { type: "env", names: ["AZURE_API_KEY", "AZURE_COGNITIVE_SERVICES_API_KEY"] }],
  261. })
  262. expect(yield* integrations.get(Integration.ID.make("google-vertex"))).toBeDefined()
  263. expect(yield* integrations.get(Integration.ID.make("azure-cognitive-services"))).toBeUndefined()
  264. expect(yield* integrations.get(Integration.ID.make("google-vertex-anthropic"))).toBeUndefined()
  265. expect(ProviderPlugins.map((plugin) => plugin.id)).not.toContain("opencode.provider.azure-cognitive-services")
  266. expect(ProviderPlugins.map((plugin) => plugin.id)).not.toContain("opencode.provider.google-vertex-anthropic")
  267. }),
  268. )
  269. it.effect("converts reasoning options into settings variants", () =>
  270. Effect.gen(function* () {
  271. const catalog = yield* Catalog.Service
  272. const integrations = yield* Integration.Service
  273. yield* ModelsDevPlugin.effect(
  274. host({
  275. catalog: catalogHost(catalog),
  276. integration: integrationHost(integrations),
  277. }),
  278. )
  279. const model = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning"))
  280. expect(model?.variants?.map((variant) => variant.id)).toEqual([
  281. Model.VariantID.make("low"),
  282. Model.VariantID.make("high"),
  283. ])
  284. expect(model?.variants).toContainEqual({
  285. id: Model.VariantID.make("low"),
  286. settings: {
  287. reasoningEffort: "low",
  288. reasoningSummary: "auto",
  289. include: ["reasoning.encrypted_content"],
  290. },
  291. })
  292. expect(model?.variants).toContainEqual({
  293. id: Model.VariantID.make("high"),
  294. settings: {
  295. reasoningEffort: "high",
  296. reasoningSummary: "auto",
  297. include: ["reasoning.encrypted_content"],
  298. },
  299. })
  300. const mode = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning-high"))
  301. expect(mode).toMatchObject({
  302. id: "gpt-reasoning-high",
  303. name: "GPT Reasoning High",
  304. headers: { "x-mode": "high" },
  305. body: { service_tier: "priority" },
  306. })
  307. expect(mode?.variants?.map((variant) => variant.id)).toEqual([
  308. Model.VariantID.make("low"),
  309. Model.VariantID.make("high"),
  310. ])
  311. const pro = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning-pro"))
  312. expect(pro).toMatchObject({
  313. id: "gpt-reasoning-pro",
  314. body: { reasoning: { mode: "pro" } },
  315. })
  316. const budgetModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-budget"))
  317. expect(budgetModel?.variants).toContainEqual({
  318. id: Model.VariantID.make("high"),
  319. settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
  320. })
  321. expect(budgetModel?.variants).toContainEqual({
  322. id: Model.VariantID.make("max"),
  323. settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
  324. })
  325. const anthropicEffortModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4.7"))
  326. expect(anthropicEffortModel?.variants).toEqual([
  327. { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
  328. {
  329. id: Model.VariantID.make("low"),
  330. settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
  331. },
  332. ])
  333. const anthropicToggleModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-toggle"))
  334. expect(anthropicToggleModel?.variants).toEqual([
  335. { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
  336. {
  337. id: Model.VariantID.make("thinking"),
  338. settings: { thinking: { type: "adaptive", display: "summarized" } },
  339. },
  340. ])
  341. const opus45 = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4-5"))
  342. expect(opus45?.variants).toEqual([
  343. { id: Model.VariantID.make("low"), settings: { effort: "low" } },
  344. { id: Model.VariantID.make("high"), settings: { effort: "high" } },
  345. ])
  346. const grok = yield* catalog.model.get(Provider.ID.make("xai"), Model.ID.make("grok-4.5"))
  347. expect(grok?.variants).toEqual(
  348. ["low", "medium", "high"].map((id) => ({
  349. id: Model.VariantID.make(id),
  350. settings: { reasoningEffort: id },
  351. })),
  352. )
  353. const minimax = yield* catalog.model.get(Provider.ID.make("opencode-go"), Model.ID.make("minimax-m3"))
  354. expect(minimax?.variants).toEqual([
  355. { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
  356. {
  357. id: Model.VariantID.make("thinking"),
  358. settings: { thinking: { type: "adaptive", display: "summarized" } },
  359. },
  360. ])
  361. const toggle = yield* catalog.model.get(Provider.ID.make("alibaba"), Model.ID.make("toggle-only"))
  362. expect(toggle?.variants).toEqual([
  363. { id: Model.VariantID.make("none"), settings: { enableThinking: false } },
  364. { id: Model.VariantID.make("thinking"), settings: { enableThinking: true } },
  365. ])
  366. const combined = yield* catalog.model.get(Provider.ID.make("alibaba"), Model.ID.make("toggle-budget"))
  367. expect(combined?.variants).toEqual([
  368. { id: Model.VariantID.make("none"), settings: { enableThinking: false } },
  369. {
  370. id: Model.VariantID.make("high"),
  371. settings: { enableThinking: true, thinkingBudget: 8000 },
  372. },
  373. {
  374. id: Model.VariantID.make("max"),
  375. settings: { enableThinking: true, thinkingBudget: 16000 },
  376. },
  377. ])
  378. const gateway = yield* catalog.model.get(Provider.ID.make("vercel"), Model.ID.make("alibaba/qwen-toggle"))
  379. expect(gateway?.variants).toEqual([
  380. { id: Model.VariantID.make("none"), settings: { enableThinking: false } },
  381. {
  382. id: Model.VariantID.make("high"),
  383. settings: { enableThinking: true, thinkingBudget: 8000 },
  384. },
  385. {
  386. id: Model.VariantID.make("max"),
  387. settings: { enableThinking: true, thinkingBudget: 16000 },
  388. },
  389. ])
  390. const gatewayNova = yield* catalog.model.get(Provider.ID.make("vercel"), Model.ID.make("amazon/nova-2-lite"))
  391. expect(gatewayNova?.variants).toEqual([
  392. {
  393. id: Model.VariantID.make("none"),
  394. settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
  395. },
  396. {
  397. id: Model.VariantID.make("low"),
  398. settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
  399. },
  400. {
  401. id: Model.VariantID.make("high"),
  402. settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
  403. },
  404. ])
  405. const gatewayFallback = yield* catalog.model.get(
  406. Provider.ID.make("vercel"),
  407. Model.ID.make("deepseek/deepseek-toggle"),
  408. )
  409. expect(gatewayFallback?.variants).toEqual([
  410. {
  411. id: Model.VariantID.make("none"),
  412. settings: { reasoning: { enabled: false } },
  413. },
  414. {
  415. id: Model.VariantID.make("low"),
  416. settings: { reasoningEffort: "low" },
  417. },
  418. {
  419. id: Model.VariantID.make("high"),
  420. settings: { reasoningEffort: "high" },
  421. },
  422. ])
  423. const openrouter = yield* catalog.model.get(Provider.ID.make("openrouter"), Model.ID.make("openrouter-toggle"))
  424. expect(openrouter?.variants).toEqual([
  425. { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
  426. { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
  427. ])
  428. const google = yield* catalog.model.get(Provider.ID.make("google"), Model.ID.make("gemini-2.5-flash"))
  429. expect(google?.variants).toEqual([
  430. {
  431. id: Model.VariantID.make("none"),
  432. settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
  433. },
  434. {
  435. id: Model.VariantID.make("high"),
  436. settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
  437. },
  438. {
  439. id: Model.VariantID.make("max"),
  440. settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
  441. },
  442. ])
  443. const vertex = yield* catalog.model.get(Provider.ID.make("google-vertex"), Model.ID.make("gemini-2.5-flash-lite"))
  444. expect(vertex?.variants).toEqual([
  445. {
  446. id: Model.VariantID.make("none"),
  447. settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
  448. },
  449. {
  450. id: Model.VariantID.make("high"),
  451. settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
  452. },
  453. {
  454. id: Model.VariantID.make("max"),
  455. settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
  456. },
  457. ])
  458. const bedrock = yield* catalog.model.get(
  459. Provider.ID.make("amazon-bedrock"),
  460. Model.ID.make("amazon.nova-2-lite-v1:0"),
  461. )
  462. expect(bedrock?.variants).toEqual([
  463. {
  464. id: Model.VariantID.make("none"),
  465. settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
  466. },
  467. {
  468. id: Model.VariantID.make("low"),
  469. settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
  470. },
  471. {
  472. id: Model.VariantID.make("high"),
  473. settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
  474. },
  475. ])
  476. const sapGemini = yield* catalog.model.get(Provider.ID.make("sap-ai-core"), Model.ID.make("gemini-2.5-flash"))
  477. expect(sapGemini?.variants).toEqual([
  478. {
  479. id: Model.VariantID.make("none"),
  480. settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
  481. },
  482. {
  483. id: Model.VariantID.make("high"),
  484. settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } },
  485. },
  486. {
  487. id: Model.VariantID.make("max"),
  488. settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
  489. },
  490. ])
  491. const sapNova = yield* catalog.model.get(Provider.ID.make("sap-ai-core"), Model.ID.make("amazon--nova-lite"))
  492. expect(sapNova?.variants).toEqual([
  493. {
  494. id: Model.VariantID.make("none"),
  495. settings: {
  496. modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
  497. },
  498. },
  499. {
  500. id: Model.VariantID.make("low"),
  501. settings: {
  502. modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } },
  503. },
  504. },
  505. {
  506. id: Model.VariantID.make("high"),
  507. settings: {
  508. modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } },
  509. },
  510. },
  511. ])
  512. const sapCohere = yield* catalog.model.get(
  513. Provider.ID.make("sap-ai-core"),
  514. Model.ID.make("cohere--command-a-reasoning"),
  515. )
  516. expect(sapCohere?.variants).toEqual([
  517. {
  518. id: Model.VariantID.make("none"),
  519. settings: { modelParams: { thinking: { type: "disabled" } } },
  520. },
  521. {
  522. id: Model.VariantID.make("low"),
  523. settings: { modelParams: { reasoning_effort: "low" } },
  524. },
  525. {
  526. id: Model.VariantID.make("high"),
  527. settings: { modelParams: { reasoning_effort: "high" } },
  528. },
  529. ])
  530. const sapAnthropicEffort = yield* catalog.model.get(
  531. Provider.ID.make("sap-ai-core"),
  532. Model.ID.make("anthropic--claude-4.7-opus"),
  533. )
  534. expect(sapAnthropicEffort?.variants).toEqual([
  535. {
  536. id: Model.VariantID.make("low"),
  537. settings: {
  538. modelParams: {
  539. additionalModelRequestFields: {
  540. thinking: { type: "adaptive", display: "summarized" },
  541. output_config: { effort: "low" },
  542. },
  543. },
  544. },
  545. },
  546. ])
  547. const sapAnthropicBudget = yield* catalog.model.get(
  548. Provider.ID.make("sap-ai-core"),
  549. Model.ID.make("anthropic--claude-4-sonnet"),
  550. )
  551. expect(sapAnthropicBudget?.variants).toEqual([
  552. {
  553. id: Model.VariantID.make("high"),
  554. settings: {
  555. modelParams: {
  556. additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } },
  557. },
  558. },
  559. },
  560. {
  561. id: Model.VariantID.make("max"),
  562. settings: {
  563. modelParams: {
  564. additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } },
  565. },
  566. },
  567. },
  568. ])
  569. }).pipe(Effect.provide(models(path.join(import.meta.dir, "fixtures", "models-dev-reasoning.json")))),
  570. )
  571. })