diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index f935ab4857..eb45850dc7 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1577,6 +1577,16 @@ export namespace ACP { if (specified && !providers.length) return specified + // altimate_change start — default to altimate-backend when configured and no model chosen yet + const altimateProvider = providers.find((p) => p.id === "altimate-backend") + if (altimateProvider && altimateProvider.models["altimate-default"]) { + return { + providerID: ProviderID.make("altimate-backend"), + modelID: ModelID.make("altimate-default"), + } + } + // altimate_change end + const opencodeProvider = providers.find((p) => p.id === "opencode") if (opencodeProvider) { if (opencodeProvider.models["big-pickle"]) { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9e81a4ff38..6221bec5ff 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1637,6 +1637,21 @@ export namespace Provider { return { providerID: entry.providerID, modelID: entry.modelID } } + // altimate_change start — default to altimate-backend when configured and no model chosen yet + const altimateProviderID = ProviderID.make("altimate-backend") + const altimateProvider = providers[altimateProviderID] + if ( + altimateProvider && + altimateProvider.models[ModelID.make("altimate-default")] && + (!cfg.provider || Object.keys(cfg.provider).includes(String(altimateProviderID))) + ) { + return { + providerID: altimateProviderID, + modelID: ModelID.make("altimate-default"), + } + } + // altimate_change end + const provider = Object.values(providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)) if (!provider) throw new Error("no providers found") const [model] = sort(Object.values(provider.models)) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 05a6064051..42dd6c437e 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "bun:test" import path from "path" +import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" @@ -2330,4 +2331,158 @@ test("github-copilot is excluded when CODESPACES=true and only GITHUB_TOKEN is s }, }) }) + +// altimate_change start — tests for altimate-backend default model preference +test("defaultModel returns altimate-backend when altimate credentials exist and no model configured", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + }), + ) + }, + }) + const originalHome = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + const altimateDir = path.join(tmp.path, ".altimate") + await fs.mkdir(altimateDir, { recursive: true }) + await Bun.write( + path.join(altimateDir, "altimate.json"), + JSON.stringify({ + altimateUrl: "https://test.altimate.ai", + altimateInstanceName: "test-instance", + altimateApiKey: "test-api-key", + }), + ) + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const model = await Provider.defaultModel() + expect(String(model.providerID)).toBe("altimate-backend") + expect(String(model.modelID)).toBe("altimate-default") + }, + }) + } finally { + if (originalHome === undefined) delete process.env.OPENCODE_TEST_HOME + else process.env.OPENCODE_TEST_HOME = originalHome + } +}) + +test("defaultModel prefers altimate-backend over other providers when altimate is configured", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + }), + ) + }, + }) + const originalHome = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + const altimateDir = path.join(tmp.path, ".altimate") + await fs.mkdir(altimateDir, { recursive: true }) + await Bun.write( + path.join(altimateDir, "altimate.json"), + JSON.stringify({ + altimateUrl: "https://test.altimate.ai", + altimateInstanceName: "test-instance", + altimateApiKey: "test-api-key", + }), + ) + try { + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + // Both providers should be available + expect(providers["anthropic"]).toBeDefined() + expect(providers["altimate-backend"]).toBeDefined() + // But defaultModel should prefer altimate-backend + const model = await Provider.defaultModel() + expect(String(model.providerID)).toBe("altimate-backend") + expect(String(model.modelID)).toBe("altimate-default") + }, + }) + } finally { + if (originalHome === undefined) delete process.env.OPENCODE_TEST_HOME + else process.env.OPENCODE_TEST_HOME = originalHome + } +}) + +test("defaultModel respects explicit config model over altimate-backend", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + model: "anthropic/claude-sonnet-4-20250514", + }), + ) + }, + }) + const originalHome = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + const altimateDir = path.join(tmp.path, ".altimate") + await fs.mkdir(altimateDir, { recursive: true }) + await Bun.write( + path.join(altimateDir, "altimate.json"), + JSON.stringify({ + altimateUrl: "https://test.altimate.ai", + altimateInstanceName: "test-instance", + altimateApiKey: "test-api-key", + }), + ) + try { + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model = await Provider.defaultModel() + expect(String(model.providerID)).toBe("anthropic") + expect(String(model.modelID)).toBe("claude-sonnet-4-20250514") + }, + }) + } finally { + if (originalHome === undefined) delete process.env.OPENCODE_TEST_HOME + else process.env.OPENCODE_TEST_HOME = originalHome + } +}) + +test("defaultModel falls through to other providers when altimate is not configured", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.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() + // altimate-backend should NOT be available (no credentials file) + expect(providers["altimate-backend"]).toBeUndefined() + const model = await Provider.defaultModel() + // Should fall through to anthropic + expect(String(model.providerID)).toBe("anthropic") + }, + }) +}) // altimate_change end