| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127 |
- import { test, expect } from "bun:test"
- import path from "path"
- import { tmpdir } from "../fixture/fixture"
- import { Instance } from "../../src/project/instance"
- import { Provider } from "../../src/provider/provider"
- import { Env } from "../../src/env"
- test("provider loaded from env variable", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- // Note: source becomes "custom" because CUSTOM_LOADERS run after env loading
- // and anthropic has a custom loader that merges additional options
- expect(providers["anthropic"].source).toBe("custom")
- },
- })
- })
- test("provider loaded from config with apiKey option", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- options: {
- apiKey: "config-api-key",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- },
- })
- })
- test("disabled_providers excludes provider", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- disabled_providers: ["anthropic"],
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeUndefined()
- },
- })
- })
- test("enabled_providers restricts to only listed providers", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- enabled_providers: ["anthropic"],
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- Env.set("OPENAI_API_KEY", "test-openai-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- expect(providers["openai"]).toBeUndefined()
- },
- })
- })
- test("model whitelist filters models for provider", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- whitelist: ["claude-sonnet-4-20250514"],
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- const models = Object.keys(providers["anthropic"].models)
- expect(models).toContain("claude-sonnet-4-20250514")
- expect(models.length).toBe(1)
- },
- })
- })
- test("model blacklist excludes specific models", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- blacklist: ["claude-sonnet-4-20250514"],
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- const models = Object.keys(providers["anthropic"].models)
- expect(models).not.toContain("claude-sonnet-4-20250514")
- },
- })
- })
- test("custom model alias via config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "my-alias": {
- id: "claude-sonnet-4-20250514",
- name: "My Custom Alias",
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- expect(providers["anthropic"].models["my-alias"]).toBeDefined()
- expect(providers["anthropic"].models["my-alias"].name).toBe("My Custom Alias")
- },
- })
- })
- test("custom provider with npm package", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "custom-provider": {
- name: "Custom Provider",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.custom.com/v1",
- env: ["CUSTOM_API_KEY"],
- models: {
- "custom-model": {
- name: "Custom Model",
- tool_call: true,
- limit: {
- context: 128000,
- output: 4096,
- },
- },
- },
- options: {
- apiKey: "custom-key",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["custom-provider"]).toBeDefined()
- expect(providers["custom-provider"].name).toBe("Custom Provider")
- expect(providers["custom-provider"].models["custom-model"]).toBeDefined()
- },
- })
- })
- test("env variable takes precedence, config merges options", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- options: {
- timeout: 60000,
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "env-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- // Config options should be merged
- expect(providers["anthropic"].options.timeout).toBe(60000)
- },
- })
- })
- test("getModel returns model for valid provider/model", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
- expect(model).toBeDefined()
- expect(model.providerID).toBe("anthropic")
- expect(model.id).toBe("claude-sonnet-4-20250514")
- const language = await Provider.getLanguage(model)
- expect(language).toBeDefined()
- },
- })
- })
- test("getModel throws ModelNotFoundError for invalid model", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- expect(Provider.getModel("anthropic", "nonexistent-model")).rejects.toThrow()
- },
- })
- })
- test("getModel throws ModelNotFoundError for invalid provider", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- expect(Provider.getModel("nonexistent-provider", "some-model")).rejects.toThrow()
- },
- })
- })
- test("parseModel correctly parses provider/model string", () => {
- const result = Provider.parseModel("anthropic/claude-sonnet-4")
- expect(result.providerID).toBe("anthropic")
- expect(result.modelID).toBe("claude-sonnet-4")
- })
- test("parseModel handles model IDs with slashes", () => {
- const result = Provider.parseModel("openrouter/anthropic/claude-3-opus")
- expect(result.providerID).toBe("openrouter")
- expect(result.modelID).toBe("anthropic/claude-3-opus")
- })
- test("defaultModel returns first available model when no config set", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const model = await Provider.defaultModel()
- expect(model.providerID).toBeDefined()
- expect(model.modelID).toBeDefined()
- },
- })
- })
- test("defaultModel respects config model setting", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- model: "anthropic/claude-sonnet-4-20250514",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const model = await Provider.defaultModel()
- expect(model.providerID).toBe("anthropic")
- expect(model.modelID).toBe("claude-sonnet-4-20250514")
- },
- })
- })
- test("provider with baseURL from config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "custom-openai": {
- name: "Custom OpenAI",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "gpt-4": {
- name: "GPT-4",
- tool_call: true,
- limit: { context: 128000, output: 4096 },
- },
- },
- options: {
- apiKey: "test-key",
- baseURL: "https://custom.openai.com/v1",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["custom-openai"]).toBeDefined()
- expect(providers["custom-openai"].options.baseURL).toBe("https://custom.openai.com/v1")
- },
- })
- })
- test("model cost defaults to zero when not specified", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "test-provider": {
- name: "Test Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "test-model": {
- name: "Test Model",
- tool_call: true,
- limit: { context: 128000, output: 4096 },
- },
- },
- options: {
- apiKey: "test-key",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["test-provider"].models["test-model"]
- expect(model.cost.input).toBe(0)
- expect(model.cost.output).toBe(0)
- expect(model.cost.cache.read).toBe(0)
- expect(model.cost.cache.write).toBe(0)
- },
- })
- })
- test("model options are merged from existing model", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- options: {
- customOption: "custom-value",
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.options.customOption).toBe("custom-value")
- },
- })
- })
- test("provider removed when all models filtered out", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- whitelist: ["nonexistent-model"],
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeUndefined()
- },
- })
- })
- test("closest finds model by partial match", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const result = await Provider.closest("anthropic", ["sonnet-4"])
- expect(result).toBeDefined()
- expect(result?.providerID).toBe("anthropic")
- expect(result?.modelID).toContain("sonnet-4")
- },
- })
- })
- test("closest returns undefined for nonexistent provider", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const result = await Provider.closest("nonexistent", ["model"])
- expect(result).toBeUndefined()
- },
- })
- })
- test("getModel uses realIdByKey for aliased models", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "my-sonnet": {
- id: "claude-sonnet-4-20250514",
- name: "My Sonnet Alias",
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"].models["my-sonnet"]).toBeDefined()
- const model = await Provider.getModel("anthropic", "my-sonnet")
- expect(model).toBeDefined()
- expect(model.id).toBe("my-sonnet")
- expect(model.name).toBe("My Sonnet Alias")
- },
- })
- })
- test("provider api field sets model api.url", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "custom-api": {
- name: "Custom API",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.example.com/v1",
- env: [],
- models: {
- "model-1": {
- name: "Model 1",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- options: {
- apiKey: "test-key",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- // api field is stored on model.api.url, used by getSDK to set baseURL
- expect(providers["custom-api"].models["model-1"].api.url).toBe("https://api.example.com/v1")
- },
- })
- })
- test("explicit baseURL overrides api field", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "custom-api": {
- name: "Custom API",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.example.com/v1",
- env: [],
- models: {
- "model-1": {
- name: "Model 1",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- options: {
- apiKey: "test-key",
- baseURL: "https://custom.override.com/v1",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["custom-api"].options.baseURL).toBe("https://custom.override.com/v1")
- },
- })
- })
- test("model inherits properties from existing database model", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- name: "Custom Name for Sonnet",
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.name).toBe("Custom Name for Sonnet")
- expect(model.capabilities.toolcall).toBe(true)
- expect(model.capabilities.attachment).toBe(true)
- expect(model.limit.context).toBeGreaterThan(0)
- },
- })
- })
- test("disabled_providers prevents loading even with env var", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- disabled_providers: ["openai"],
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("OPENAI_API_KEY", "test-openai-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["openai"]).toBeUndefined()
- },
- })
- })
- test("enabled_providers with empty array allows no providers", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- enabled_providers: [],
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- Env.set("OPENAI_API_KEY", "test-openai-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(Object.keys(providers).length).toBe(0)
- },
- })
- })
- test("whitelist and blacklist can be combined", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- whitelist: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"],
- blacklist: ["claude-opus-4-20250514"],
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- const models = Object.keys(providers["anthropic"].models)
- expect(models).toContain("claude-sonnet-4-20250514")
- expect(models).not.toContain("claude-opus-4-20250514")
- expect(models.length).toBe(1)
- },
- })
- })
- test("model modalities default correctly", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "test-provider": {
- name: "Test",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "test-model": {
- name: "Test Model",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["test-provider"].models["test-model"]
- expect(model.capabilities.input.text).toBe(true)
- expect(model.capabilities.output.text).toBe(true)
- },
- })
- })
- test("model with custom cost values", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "test-provider": {
- name: "Test",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "test-model": {
- name: "Test Model",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- cost: {
- input: 5,
- output: 15,
- cache_read: 2.5,
- cache_write: 7.5,
- },
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["test-provider"].models["test-model"]
- expect(model.cost.input).toBe(5)
- expect(model.cost.output).toBe(15)
- expect(model.cost.cache.read).toBe(2.5)
- expect(model.cost.cache.write).toBe(7.5)
- },
- })
- })
- test("getSmallModel returns appropriate small model", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const model = await Provider.getSmallModel("anthropic")
- expect(model).toBeDefined()
- expect(model?.id).toContain("haiku")
- },
- })
- })
- test("getSmallModel respects config small_model override", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- small_model: "anthropic/claude-sonnet-4-20250514",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const model = await Provider.getSmallModel("anthropic")
- expect(model).toBeDefined()
- expect(model?.providerID).toBe("anthropic")
- expect(model?.id).toBe("claude-sonnet-4-20250514")
- },
- })
- })
- test("provider.sort prioritizes preferred models", () => {
- const models = [
- { id: "random-model", name: "Random" },
- { id: "claude-sonnet-4-latest", name: "Claude Sonnet 4" },
- { id: "gpt-5-turbo", name: "GPT-5 Turbo" },
- { id: "other-model", name: "Other" },
- ] as any[]
- const sorted = Provider.sort(models)
- expect(sorted[0].id).toContain("sonnet-4")
- expect(sorted[0].id).toContain("latest")
- expect(sorted[sorted.length - 1].id).not.toContain("gpt-5")
- expect(sorted[sorted.length - 1].id).not.toContain("sonnet-4")
- })
- test("multiple providers can be configured simultaneously", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- options: { timeout: 30000 },
- },
- openai: {
- options: { timeout: 60000 },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-anthropic-key")
- Env.set("OPENAI_API_KEY", "test-openai-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"]).toBeDefined()
- expect(providers["openai"]).toBeDefined()
- expect(providers["anthropic"].options.timeout).toBe(30000)
- expect(providers["openai"].options.timeout).toBe(60000)
- },
- })
- })
- test("provider with custom npm package", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "local-llm": {
- name: "Local LLM",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "llama-3": {
- name: "Llama 3",
- tool_call: true,
- limit: { context: 8192, output: 2048 },
- },
- },
- options: {
- apiKey: "not-needed",
- baseURL: "http://localhost:11434/v1",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["local-llm"]).toBeDefined()
- expect(providers["local-llm"].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
- expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1")
- },
- })
- })
- // Edge cases for model configuration
- test("model alias name defaults to alias key when id differs", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- sonnet: {
- id: "claude-sonnet-4-20250514",
- // no name specified - should default to "sonnet" (the key)
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["anthropic"].models["sonnet"].name).toBe("sonnet")
- },
- })
- })
- test("provider with multiple env var options only includes apiKey when single env", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "multi-env": {
- name: "Multi Env Provider",
- npm: "@ai-sdk/openai-compatible",
- env: ["MULTI_ENV_KEY_1", "MULTI_ENV_KEY_2"],
- models: {
- "model-1": {
- name: "Model 1",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- options: {
- baseURL: "https://api.example.com/v1",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("MULTI_ENV_KEY_1", "test-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["multi-env"]).toBeDefined()
- // When multiple env options exist, key should NOT be auto-set
- expect(providers["multi-env"].key).toBeUndefined()
- },
- })
- })
- test("provider with single env var includes apiKey automatically", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "single-env": {
- name: "Single Env Provider",
- npm: "@ai-sdk/openai-compatible",
- env: ["SINGLE_ENV_KEY"],
- models: {
- "model-1": {
- name: "Model 1",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- options: {
- baseURL: "https://api.example.com/v1",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("SINGLE_ENV_KEY", "my-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["single-env"]).toBeDefined()
- // Single env option should auto-set key
- expect(providers["single-env"].key).toBe("my-api-key")
- },
- })
- })
- test("model cost overrides existing cost values", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- cost: {
- input: 999,
- output: 888,
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.cost.input).toBe(999)
- expect(model.cost.output).toBe(888)
- },
- })
- })
- test("completely new provider not in database can be configured", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "brand-new-provider": {
- name: "Brand New",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- api: "https://new-api.com/v1",
- models: {
- "new-model": {
- name: "New Model",
- tool_call: true,
- reasoning: true,
- attachment: true,
- temperature: true,
- limit: { context: 32000, output: 8000 },
- modalities: {
- input: ["text", "image"],
- output: ["text"],
- },
- },
- },
- options: {
- apiKey: "new-key",
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["brand-new-provider"]).toBeDefined()
- expect(providers["brand-new-provider"].name).toBe("Brand New")
- const model = providers["brand-new-provider"].models["new-model"]
- expect(model.capabilities.reasoning).toBe(true)
- expect(model.capabilities.attachment).toBe(true)
- expect(model.capabilities.input.image).toBe(true)
- },
- })
- })
- test("disabled_providers and enabled_providers interaction", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- // enabled_providers takes precedence - only these are considered
- enabled_providers: ["anthropic", "openai"],
- // Then disabled_providers filters from the enabled set
- disabled_providers: ["openai"],
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-anthropic")
- Env.set("OPENAI_API_KEY", "test-openai")
- Env.set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
- },
- fn: async () => {
- const providers = await Provider.list()
- // anthropic: in enabled, not in disabled = allowed
- expect(providers["anthropic"]).toBeDefined()
- // openai: in enabled, but also in disabled = NOT allowed
- expect(providers["openai"]).toBeUndefined()
- // google: not in enabled = NOT allowed (even though not disabled)
- expect(providers["google"]).toBeUndefined()
- },
- })
- })
- test("model with tool_call false", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "no-tools": {
- name: "No Tools Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "basic-model": {
- name: "Basic Model",
- tool_call: false,
- limit: { context: 4000, output: 1000 },
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["no-tools"].models["basic-model"].capabilities.toolcall).toBe(false)
- },
- })
- })
- test("model defaults tool_call to true when not specified", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "default-tools": {
- name: "Default Tools Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- model: {
- name: "Model",
- // tool_call not specified
- limit: { context: 4000, output: 1000 },
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["default-tools"].models["model"].capabilities.toolcall).toBe(true)
- },
- })
- })
- test("model headers are preserved", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "headers-provider": {
- name: "Headers Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- model: {
- name: "Model",
- tool_call: true,
- limit: { context: 4000, output: 1000 },
- headers: {
- "X-Custom-Header": "custom-value",
- Authorization: "Bearer special-token",
- },
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["headers-provider"].models["model"]
- expect(model.headers).toEqual({
- "X-Custom-Header": "custom-value",
- Authorization: "Bearer special-token",
- })
- },
- })
- })
- test("provider env fallback - second env var used if first missing", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "fallback-env": {
- name: "Fallback Env Provider",
- npm: "@ai-sdk/openai-compatible",
- env: ["PRIMARY_KEY", "FALLBACK_KEY"],
- models: {
- model: {
- name: "Model",
- tool_call: true,
- limit: { context: 4000, output: 1000 },
- },
- },
- options: { baseURL: "https://api.example.com" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- // Only set fallback, not primary
- Env.set("FALLBACK_KEY", "fallback-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- // Provider should load because fallback env var is set
- expect(providers["fallback-env"]).toBeDefined()
- },
- })
- })
- test("getModel returns consistent results", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
- const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
- expect(model1.providerID).toEqual(model2.providerID)
- expect(model1.id).toEqual(model2.id)
- expect(model1).toEqual(model2)
- },
- })
- })
- test("provider name defaults to id when not in database", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "my-custom-id": {
- // no name specified
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- model: {
- name: "Model",
- tool_call: true,
- limit: { context: 4000, output: 1000 },
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["my-custom-id"].name).toBe("my-custom-id")
- },
- })
- })
- test("ModelNotFoundError includes suggestions for typos", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- try {
- await Provider.getModel("anthropic", "claude-sonet-4") // typo: sonet instead of sonnet
- expect(true).toBe(false) // Should not reach here
- } catch (e: any) {
- expect(e.data.suggestions).toBeDefined()
- expect(e.data.suggestions.length).toBeGreaterThan(0)
- }
- },
- })
- })
- test("ModelNotFoundError for provider includes suggestions", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- try {
- await Provider.getModel("antropic", "claude-sonnet-4") // typo: antropic
- expect(true).toBe(false) // Should not reach here
- } catch (e: any) {
- expect(e.data.suggestions).toBeDefined()
- expect(e.data.suggestions).toContain("anthropic")
- }
- },
- })
- })
- test("getProvider returns undefined for nonexistent provider", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const provider = await Provider.getProvider("nonexistent")
- expect(provider).toBeUndefined()
- },
- })
- })
- test("getProvider returns provider info", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const provider = await Provider.getProvider("anthropic")
- expect(provider).toBeDefined()
- expect(provider?.id).toBe("anthropic")
- },
- })
- })
- test("closest returns undefined when no partial match found", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const result = await Provider.closest("anthropic", ["nonexistent-xyz-model"])
- expect(result).toBeUndefined()
- },
- })
- })
- test("closest checks multiple query terms in order", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- // First term won't match, second will
- const result = await Provider.closest("anthropic", ["nonexistent", "haiku"])
- expect(result).toBeDefined()
- expect(result?.modelID).toContain("haiku")
- },
- })
- })
- test("model limit defaults to zero when not specified", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "no-limit": {
- name: "No Limit Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- model: {
- name: "Model",
- tool_call: true,
- // no limit specified
- },
- },
- options: { apiKey: "test" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["no-limit"].models["model"]
- expect(model.limit.context).toBe(0)
- expect(model.limit.output).toBe(0)
- },
- })
- })
- test("provider options are deeply merged", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- options: {
- headers: {
- "X-Custom": "custom-value",
- },
- timeout: 30000,
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- // Custom options should be merged
- expect(providers["anthropic"].options.timeout).toBe(30000)
- expect(providers["anthropic"].options.headers["X-Custom"]).toBe("custom-value")
- // anthropic custom loader adds its own headers, they should coexist
- expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined()
- },
- })
- })
- test("custom model inherits npm package from models.dev provider config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- openai: {
- models: {
- "my-custom-model": {
- name: "My Custom Model",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("OPENAI_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["openai"].models["my-custom-model"]
- expect(model).toBeDefined()
- expect(model.api.npm).toBe("@ai-sdk/openai")
- },
- })
- })
- test("custom model inherits api.url from models.dev provider", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- openrouter: {
- models: {
- "prime-intellect/intellect-3": {},
- "deepseek/deepseek-r1-0528": {
- name: "DeepSeek R1",
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("OPENROUTER_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- expect(providers["openrouter"]).toBeDefined()
- // New model not in database should inherit api.url from provider
- const intellect = providers["openrouter"].models["prime-intellect/intellect-3"]
- expect(intellect).toBeDefined()
- expect(intellect.api.url).toBe("https://openrouter.ai/api/v1")
- // Another new model should also inherit api.url
- const deepseek = providers["openrouter"].models["deepseek/deepseek-r1-0528"]
- expect(deepseek).toBeDefined()
- expect(deepseek.api.url).toBe("https://openrouter.ai/api/v1")
- expect(deepseek.name).toBe("DeepSeek R1")
- },
- })
- })
- test("model variants are generated for reasoning models", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- // Claude sonnet 4 has reasoning capability
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.capabilities.reasoning).toBe(true)
- expect(model.variants).toBeDefined()
- expect(Object.keys(model.variants!).length).toBeGreaterThan(0)
- },
- })
- })
- test("model variants can be disabled via config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- variants: {
- high: { disabled: true },
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.variants).toBeDefined()
- expect(model.variants!["high"]).toBeUndefined()
- // max variant should still exist
- expect(model.variants!["max"]).toBeDefined()
- },
- })
- })
- test("model variants can be customized via config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- variants: {
- high: {
- thinking: {
- type: "enabled",
- budgetTokens: 20000,
- },
- },
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.variants!["high"]).toBeDefined()
- expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
- },
- })
- })
- test("disabled key is stripped from variant config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- variants: {
- max: {
- disabled: false,
- customField: "test",
- },
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.variants!["max"]).toBeDefined()
- expect(model.variants!["max"].disabled).toBeUndefined()
- expect(model.variants!["max"].customField).toBe("test")
- },
- })
- })
- test("all variants can be disabled via config", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- variants: {
- high: { disabled: true },
- max: { disabled: true },
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.variants).toBeDefined()
- expect(Object.keys(model.variants!).length).toBe(0)
- },
- })
- })
- test("variant config merges with generated variants", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-20250514": {
- variants: {
- high: {
- extraOption: "custom-value",
- },
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("ANTHROPIC_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
- expect(model.variants!["high"]).toBeDefined()
- // Should have both the generated thinking config and the custom option
- expect(model.variants!["high"].thinking).toBeDefined()
- expect(model.variants!["high"].extraOption).toBe("custom-value")
- },
- })
- })
- test("variants filtered in second pass for database models", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- openai: {
- models: {
- "gpt-5": {
- variants: {
- high: { disabled: true },
- },
- },
- },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- init: async () => {
- Env.set("OPENAI_API_KEY", "test-api-key")
- },
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["openai"].models["gpt-5"]
- expect(model.variants).toBeDefined()
- expect(model.variants!["high"]).toBeUndefined()
- // Other variants should still exist
- expect(model.variants!["medium"]).toBeDefined()
- },
- })
- })
- test("custom model with variants enabled and disabled", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(
- path.join(dir, "opencode.json"),
- JSON.stringify({
- $schema: "https://opencode.ai/config.json",
- provider: {
- "custom-reasoning": {
- name: "Custom Reasoning Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "reasoning-model": {
- name: "Reasoning Model",
- tool_call: true,
- reasoning: true,
- limit: { context: 128000, output: 16000 },
- variants: {
- low: { reasoningEffort: "low" },
- medium: { reasoningEffort: "medium" },
- high: { reasoningEffort: "high", disabled: true },
- custom: { reasoningEffort: "custom", budgetTokens: 5000 },
- },
- },
- },
- options: { apiKey: "test-key" },
- },
- },
- }),
- )
- },
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const providers = await Provider.list()
- const model = providers["custom-reasoning"].models["reasoning-model"]
- expect(model.variants).toBeDefined()
- // Enabled variants should exist
- expect(model.variants!["low"]).toBeDefined()
- expect(model.variants!["low"].reasoningEffort).toBe("low")
- expect(model.variants!["medium"]).toBeDefined()
- expect(model.variants!["medium"].reasoningEffort).toBe("medium")
- expect(model.variants!["custom"]).toBeDefined()
- expect(model.variants!["custom"].reasoningEffort).toBe("custom")
- expect(model.variants!["custom"].budgetTokens).toBe(5000)
- // Disabled variant should not exist
- expect(model.variants!["high"]).toBeUndefined()
- // disabled key should be stripped from all variants
- expect(model.variants!["low"].disabled).toBeUndefined()
- expect(model.variants!["medium"].disabled).toBeUndefined()
- expect(model.variants!["custom"].disabled).toBeUndefined()
- },
- })
- })
|