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() {