models-dev.test.ts 26 KB

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