diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6363986..019c5d294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ This file tracks product releases for Roomote (single monorepo version). Automated release entries are prepended by `pnpm run version`. +## 0.32.0 (2026-08-03) + +This release adds OpenCode Go subscriptions, restores convenient automation test runs, refreshes recommended models, and makes license syncing more resilient. + +### Highlights + +- Let operators connect an OpenCode Go subscription independently from OpenCode Zen and use supported Go models across Roomote task roles. +- Run enabled custom automations on demand and test newly created automations immediately. +- Use current Qwen and DeepSeek recommendations across supported model providers. +- Retry license usage reporting after transient Roomote Cloud failures. + +### Minor changes + +- Let operators connect an OpenCode Go subscription independently from OpenCode Zen and use supported Go models across Roomote task roles. + +### Patch changes + +- Let administrators run enabled custom automations on demand from Automations settings, and offer an immediate test run after an automation is created conversationally. +- Recommend Qwen3.8 Max through supported providers and use OpenCode Zen's supported DeepSeek V4 Flash model identifier. +- Retry license usage reports after transient Roomote Cloud failures and report concurrent license-key changes accurately instead of dropping usage observations or reporting a false success. + ## 0.31.0 (2026-08-03) This release expands Amazon Bedrock and deployment controls, refreshes Automations and self-hosted licensing, and adds copyable Cookbook recipes for common workflows. diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 9d49a38d1..d24437bb9 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -404,6 +404,7 @@ common provider keys into worker containers: - `ZAI_CODING_PLAN_API_KEY` (Z.AI Coding Plan, `zai-coding-plan/...` models, with `ZAI_CODING_PLAN_REGION`) - `OPENCODE_API_KEY` +- `OPENCODE_GO_API_KEY` (OpenCode Go subscription, `opencode-go/...` models) If your provider uses another env var name, list it in `R_MODEL_ENV_KEYS`: diff --git a/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts b/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts index e913f057d..fc9a5f6b2 100644 --- a/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts +++ b/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts @@ -1187,6 +1187,25 @@ describe('inference gateway', () => { expect(headers.get('authorization')).toBe('Bearer provider-secret-key'); }); + it('forwards OpenCode Go requests to the subscription endpoint', async () => { + const fetchMock = stubUpstreamFetch(); + const response = await postMessages( + createApp(createRunToken()), + '/api/inference/opencode-go/v1/responses', + ); + + expect(response.status).toBe(200); + expect(mockResolveModelProviderEnvValue).toHaveBeenCalledWith([ + 'OPENCODE_GO_API_KEY', + ]); + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe('https://opencode.ai/zen/go/v1/responses'); + + const headers = new Headers(init.headers); + expect(headers.get('authorization')).toBe('Bearer provider-secret-key'); + }); + it('preserves the query string on upstream requests', async () => { const fetchMock = stubUpstreamFetch(); const app = createApp(createRunToken()); diff --git a/apps/docs/docs.json b/apps/docs/docs.json index c369d0646..71155bbe9 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -77,6 +77,7 @@ "providers/inference/openai", "providers/inference/openai-compatible", "providers/inference/opencode", + "providers/inference/opencode-go", "providers/inference/openrouter", "providers/inference/together-ai", "providers/inference/vercel-ai-gateway", diff --git a/apps/docs/environment-variables.mdx b/apps/docs/environment-variables.mdx index 2f540d8d5..7894de280 100644 --- a/apps/docs/environment-variables.mdx +++ b/apps/docs/environment-variables.mdx @@ -201,7 +201,8 @@ as per-task auth tokens or workspace paths. | `ZAI_REGION` | Provider key | Z.AI region: `global` (International) or `china`. Defaults to `global` when unset. | | `ZAI_CODING_PLAN_API_KEY` | Provider key | Z.AI Coding Plan API key for `zai-coding-plan/...` models. Not a general platform key. | | `ZAI_CODING_PLAN_REGION` | Provider key | Z.AI Coding Plan region: `global` (International) or `china`. Defaults to `global` when unset. | -| `OPENCODE_API_KEY` | Provider key | OpenCode Zen / Go API key. | +| `OPENCODE_API_KEY` | Provider key | OpenCode Zen API key. | +| `OPENCODE_GO_API_KEY` | Provider key | OpenCode API key for an active Go subscription and `opencode-go/...` models. | | `GEMINI_API_KEY` | Provider key | Google Gemini API key. Can also be saved from **Settings > Models**. | | `GOOGLE_GENERATIVE_AI_API_KEY` | Provider key | Alternate Google/Gemini provider key forwarded when configured or inferred. | | `AWS_BEARER_TOKEN_BEDROCK` | Provider key | Amazon Bedrock Mantle API key. Can also be saved from **Settings > Models**. | diff --git a/apps/docs/models.mdx b/apps/docs/models.mdx index 959f84f64..c716bb9e9 100644 --- a/apps/docs/models.mdx +++ b/apps/docs/models.mdx @@ -51,7 +51,7 @@ These connections use metered API billing or a provider-managed gateway: | [MiniMax](/providers/inference/minimax) | MiniMax API key | MiniMax account | | [Moonshot AI (Kimi)](/providers/inference/moonshot-ai) | Kimi Open Platform key | Moonshot Open Platform balance | | [OpenAI](/providers/inference/openai) | OpenAI API key | OpenAI API project | -| [OpenCode Zen / Go](/providers/inference/opencode) | OpenCode API key | OpenCode account | +| [OpenCode Zen](/providers/inference/opencode) | OpenCode API key | OpenCode account | | [OpenRouter](/providers/inference/openrouter) | OpenRouter API key | OpenRouter balance and key limit | | [Together AI](/providers/inference/together-ai) | Together AI API key | Together AI account | | [Vercel AI Gateway](/providers/inference/vercel-ai-gateway) | AI Gateway API key | Vercel team | @@ -68,6 +68,7 @@ general API balance: | [ChatGPT Subscription](/providers/inference/chatgpt) | OpenAI device authorization | ChatGPT plan windows | | [GitHub Copilot](/providers/inference/github-copilot) | GitHub device authorization | Copilot plan allowance | | [Kimi for Coding](/providers/inference/kimi-for-coding) | Kimi membership key | Coding membership allowance | +| [OpenCode Go](/providers/inference/opencode-go) | OpenCode API key | Go plan allowance | | [xAI Grok Subscription](/providers/inference/xai-subscription) | xAI account authorization | SuperGrok or eligible X plan allowance | | [Z.AI Coding Plan](/providers/inference/zai-coding-plan) | Regional Coding Plan key | Coding Plan quota windows | diff --git a/apps/docs/providers/inference/opencode-go.mdx b/apps/docs/providers/inference/opencode-go.mdx new file mode 100644 index 000000000..3e0dd18f6 --- /dev/null +++ b/apps/docs/providers/inference/opencode-go.mdx @@ -0,0 +1,55 @@ +--- +title: OpenCode Go +icon: 'https://unpkg.com/@lobehub/icons-static-svg@1.94.0/icons/opencode.svg' +description: Use an OpenCode Go subscription with Roomote. +--- + +OpenCode Go is a subscription for a curated catalog of coding models. Use this +provider when you want Roomote tasks to consume the allowance included with +your Go plan. + +## Subscribe and get an API key + +Sign in to [OpenCode](https://opencode.ai/auth), subscribe to Go, and copy your +API key. OpenCode currently allows one Go subscription per workspace. + +## Configuration + +Add **OpenCode Go** in **Settings > Models** and paste the key, or set: + +```sh +OPENCODE_GO_API_KEY=... +``` + +Roomote uses a separate environment variable so Zen and Go can be configured +independently. Paste the OpenCode API key issued for the workspace; Roomote +binds it to OpenCode's `opencode-go` provider at runtime. + +Go models use the `opencode-go/` prefix. Connecting the provider adds a curated +set of eligible models and recommends lower-cost options for helper work. + +## Usage behavior + +Requests consume the rolling and monthly allowances defined by your Go plan. +The OpenCode account console is authoritative for current usage and resets. If +you enable **Use balance** in OpenCode, requests can fall back to your Zen +balance after the included allowance is exhausted. + +See [OpenCode Go documentation](https://opencode.ai/docs/go/) for current +models, limits, pricing, and retention details. + +## Verify setup + +1. save `OPENCODE_GO_API_KEY` +2. confirm OpenCode Go models appear in **Settings > Models** +3. apply a preset or assign an `opencode-go/...` model manually +4. run a small task and confirm it appears in OpenCode usage + +## Common issues + +- **The key is rejected.** Confirm the workspace has an active Go subscription + and that you copied its OpenCode API key. +- **A model is unavailable.** Go's catalog can change; select another model + shown in the OpenCode Go console. +- **Requests stop after sustained use.** Check the plan's rolling, weekly, and + monthly allowance windows in OpenCode. diff --git a/apps/docs/providers/inference/opencode.mdx b/apps/docs/providers/inference/opencode.mdx index cf8b8093c..f3c9685f1 100644 --- a/apps/docs/providers/inference/opencode.mdx +++ b/apps/docs/providers/inference/opencode.mdx @@ -1,5 +1,5 @@ --- -title: OpenCode Zen / Go +title: OpenCode Zen icon: 'https://unpkg.com/@lobehub/icons-static-svg@1.94.0/icons/opencode.svg' description: Use OpenCode's routed Zen model service with Roomote. --- @@ -16,13 +16,13 @@ selected models. ## Configuration -Add **OpenCode Zen / Go** in **Settings > Models** and paste the key, or set: +Add **OpenCode Zen** in **Settings > Models** and paste the key, or set: ```sh OPENCODE_API_KEY=... ``` -Roomote exposes routed models under the `opencode/` prefix and adds a recommended +Roomote exposes routed Zen models under the `opencode/` prefix and adds a recommended set after connection. The preset can use inexpensive routed models for helper work and stronger models for coding, planning, vision, and review. diff --git a/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx b/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx index fbcc143e6..6ef8b7678 100644 --- a/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx +++ b/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx @@ -52,6 +52,7 @@ describe('SetupDocs', () => { ['openai', 'openai'], ['openai-compatible', 'openai-compatible'], ['opencode', 'opencode'], + ['opencode-go', 'opencode-go'], ['openrouter', 'openrouter'], ['togetherai', 'together-ai'], ['vercel', 'vercel-ai-gateway'], diff --git a/apps/web/src/app/(onboarding)/setup/setup-docs.ts b/apps/web/src/app/(onboarding)/setup/setup-docs.ts index acba0809f..fa9db79aa 100644 --- a/apps/web/src/app/(onboarding)/setup/setup-docs.ts +++ b/apps/web/src/app/(onboarding)/setup/setup-docs.ts @@ -66,6 +66,7 @@ const MODEL_PROVIDER_DOC_PATHS: Partial< openai: 'providers/inference/openai', 'openai-compatible': 'providers/inference/openai-compatible', opencode: 'providers/inference/opencode', + 'opencode-go': 'providers/inference/opencode-go', openrouter: 'providers/inference/openrouter', togetherai: 'providers/inference/together-ai', vercel: 'providers/inference/vercel-ai-gateway', diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx index eede21ef9..2798b8fc7 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx @@ -241,6 +241,7 @@ const mutations = vi.hoisted(() => ({ connectSlack: vi.fn(), updateSettings: vi.fn(), triggerAgent: vi.fn(), + triggerCustomAutomation: vi.fn(), latestSettingsOptions: null as { onSuccess?: ( result: NonNullable, @@ -359,11 +360,16 @@ vi.mock('@tanstack/react-query', () => ({ result: NonNullable, ) => void; onError?: (...args: unknown[]) => void; + mutationKind?: 'triggerCustomAutomation'; }) => { return { isPending: false, mutate: vi.fn((variables: unknown) => { - mutations.updateSettings(variables); + if (_options?.mutationKind === 'triggerCustomAutomation') { + mutations.triggerCustomAutomation(variables); + } else { + mutations.updateSettings(variables); + } }), }; }, @@ -412,7 +418,10 @@ vi.mock('@/trpc/client', () => ({ mutationOptions: (options?: Record) => options ?? {}, }, triggerCustomAutomation: { - mutationOptions: (options?: Record) => options ?? {}, + mutationOptions: (options?: Record) => ({ + ...options, + mutationKind: 'triggerCustomAutomation', + }), }, resolveCustomAutomationSchedule: { mutationOptions: (options?: Record) => options ?? {}, @@ -879,7 +888,7 @@ describe('AutomationsSettings', () => { updatedAt: new Date('2026-01-01T00:00:00Z'), }, ]; - render(); + const { rerender } = render(); expect( await screen.findByRole('switch', { @@ -891,6 +900,20 @@ describe('AutomationsSettings', () => { 'Weekly · Production · slack:#roomote-managers · Created by Ada', ), ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Run Weekly flaky-test scan now' }), + ).toBeEnabled(); + fireEvent.click( + screen.getByRole('button', { name: 'Run Weekly flaky-test scan now' }), + ); + expect(mutations.triggerCustomAutomation).toHaveBeenCalledWith({ + id: 'automation-1', + }); + state.customAutomations[0]!.enabled = false; + rerender(); + expect( + screen.getByRole('button', { name: 'Run Weekly flaky-test scan now' }), + ).toBeDisabled(); expect( screen.getByRole('button', { name: 'Configure Weekly flaky-test scan', diff --git a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx index e23fff447..a85f58c48 100644 --- a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx +++ b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx @@ -24,6 +24,7 @@ import { DialogTitle, Input, Label, + Play, Plus, Select, SelectContent, @@ -284,6 +285,35 @@ export function CustomAutomationsSection() { }), ); + const triggerMutation = useMutation( + trpc.automations.triggerCustomAutomation.mutationOptions({ + onSuccess: (result) => { + switch (result.outcome) { + case 'launched': + toast.success('Custom automation started a task.', { + action: { + label: 'Open task', + onClick: () => window.open(`/task/${result.taskId}`, '_blank'), + }, + }); + break; + case 'completed': + toast.success('Custom automation ran successfully.'); + break; + case 'skipped': + toast.info(`Custom automation had nothing to do: ${result.reason}`); + break; + case 'failed': + toast.error(`Custom automation failed: ${result.error}`); + break; + } + }, + onError: (error) => { + toast.error(error.message || 'Failed to run custom automation'); + }, + }), + ); + const resolveScheduleMutation = useMutation( trpc.automations.resolveCustomAutomationSchedule.mutationOptions({ onSuccess: (result, variables) => { @@ -336,7 +366,8 @@ export function CustomAutomationsSection() { createMutation.isPending || updateMutation.isPending || deleteMutation.isPending || - toggleMutation.isPending; + toggleMutation.isPending || + triggerMutation.isPending; const closeEditor = () => { setIsCreating(false); @@ -898,6 +929,18 @@ export function CustomAutomationsSection() {

+ + +