Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
455a996
feat(provider): add OpenCode Zen/Go subscription support
Gravirei May 24, 2026
f190744
feat(provider): add [Zen]/[Go] tags to OpenCode preset labels
Gravirei May 24, 2026
1b31f27
feat(provider): enable dynamic model discovery for OpenCode
Gravirei May 24, 2026
33ea351
test(provider): add comprehensive OpenCode Zen/Go test suite
Gravirei May 24, 2026
8603ef5
fix(provider): add per-model endpoint routing (P1)
Gravirei May 25, 2026
0e6cff7
refactor(opencode): model OpenCode Zen/Go as gateways (P2)
Gravirei May 25, 2026
aad0fb3
docs(provider): document OpenCode setup and move badge metadata to de…
Gravirei May 26, 2026
193a09c
fix(provider): match request body to endpoint format for OpenCode /me…
Gravirei May 26, 2026
5928643
fix(provider): add Google AI SDK body/response format for OpenCode Ze…
Gravirei May 26, 2026
f796b47
fix: prevent OpenCode model descriptors from shadowing canonical limits
Gravirei May 28, 2026
53cb00d
fix: align OpenCode Go descriptor metadata with Zen
Gravirei May 28, 2026
3448593
chore: merge upstream main (20 commits)
Gravirei May 28, 2026
2e58071
fix: accept OPENAI_API_KEY as fallback in OpenCode validation
Gravirei May 29, 2026
2993a3d
chore: trigger mergeability recheck
Gravirei May 29, 2026
53e67ff
chore: merge upstream main (4 commits)
Gravirei May 29, 2026
14c1c21
Merge branch 'main' into feat/opencode-zen-go-integration
Gravirei Jun 3, 2026
70dc3d9
feat(shim): forward effort/thinking to OpenCode Zen/Go endpoints
Gravirei May 26, 2026
1498b16
feat: update OpenCode Zen and Go model counts, add new models, and en…
Gravirei Jun 3, 2026
4bb3552
feat: implement xhigh effort support for specific models and adjust e…
Gravirei Jun 3, 2026
ecfa287
fix(effort): address reviewer feedback on xhigh + new models
Gravirei Jun 3, 2026
78a955e
fix(effort): address reviewer feedback on xhigh effort + new models
Gravirei Jun 3, 2026
6e2fce5
chore(effort): order xhigh before max in EFFORT_LEVELS
Gravirei Jun 3, 2026
bf45bea
chore(effort): order xhigh before max in settings + SDK schemas
Gravirei Jun 3, 2026
1aa600f
Merge remote-tracking branch 'upstream/main' into feat/opencode-zen-g…
Gravirei Jun 3, 2026
539a9d4
fix(effort): clamp ModelPicker selection and mark xhigh as current
Gravirei Jun 4, 2026
7420d65
docs(effort): fix stale EffortPicker comment about xhigh normalization
Gravirei Jun 4, 2026
3cf5de2
docs(effort): update /effort help to match xhigh support matrix
Gravirei Jun 4, 2026
857c55b
fix(sdk): address reviewer P2 — sync xhigh across override union, sch…
Gravirei Jun 4, 2026
7d65569
fix(effort): narrow allowlist to shim-serialized models; sync max des…
Gravirei Jun 5, 2026
2fcb07d
Merge remote-tracking branch 'upstream/main' into feat/opencode-zen-g…
Gravirei Jun 8, 2026
fee6c32
chore: trigger CodeRabbit re-review
Gravirei Jun 8, 2026
72fb746
fix(effort): gate modelSupportsXHighEffort on modelSupportsEffort
Gravirei Jun 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ Advanced and source-build guides:
| Codex OAuth | `/provider` | Opens ChatGPT sign-in in your browser and stores Codex credentials securely |
| Codex | `/provider` | Uses existing Codex CLI auth, OpenClaude secure storage, or env credentials |
| Gitlawb Opengateway | Startup default, `/provider`, or env vars | Smart gateway at `https://opengateway.gitlawb.com/v1`; requires an API key from https://gitlawb.com/opengateway/keys and routes Xiaomi MiMo and GMI Cloud partner models by `OPENAI_MODEL` |
| OpenCode Zen | `/provider` or env vars | Pay-as-you-go AI gateway (41 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/v1`; shared key with OpenCode Go |
| OpenCode Go | `/provider` or env vars | $10/mo subscription for open models (12 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/go/v1`; shared key with OpenCode Zen |
| OpenCode Zen | `/provider` or env vars | Pay-as-you-go AI gateway (43 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/v1`; shared key with OpenCode Go |
| OpenCode Go | `/provider` or env vars | $10/mo subscription for open models (13 models); uses `OPENCODE_API_KEY` via `https://opencode.ai/zen/go/v1`; shared key with OpenCode Zen |
| Xiaomi MiMo | `/provider` or env vars | OpenAI-compatible API at `https://mimo.mi.com`; uses `MIMO_API_KEY` and defaults to `mimo-v2.5-pro` |
| Ollama | `/provider` or env vars | Local inference with no API key |
| Atomic Chat | `/provider`, env vars, or `bun run dev:atomic-chat` | Local Model Provider; auto-detects loaded models |
Expand Down
4 changes: 2 additions & 2 deletions docs/advanced-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export OPENAI_MODEL=gpt-5.4
openclaude
```

OpenCode Zen is a pay-as-you-go AI gateway with 41 models (GPT, Claude, Gemini,
OpenCode Zen is a pay-as-you-go AI gateway with 43 models (GPT, Claude, Gemini,
Qwen, MiniMax, GLM, Kimi, Grok, Big Pickle, DeepSeek, Nemotron). Uses the same
`OPENCODE_API_KEY` as OpenCode Go. Get your key from https://opencode.ai.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

Expand All @@ -209,7 +209,7 @@ export OPENAI_MODEL=glm-5.1
openclaude
```

OpenCode Go is a $10/mo subscription for 12 open models (GLM, Kimi, DeepSeek,
OpenCode Go is a $10/mo subscription for 13 open models (GLM, Kimi, DeepSeek,
MiMo, MiniMax, Qwen). Uses the same `OPENCODE_API_KEY` as OpenCode Zen.

### Gitlawb Opengateway
Expand Down
2 changes: 1 addition & 1 deletion src/entrypoints/sdk/runtimeTypes.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type EffortLevel = 'low' | 'medium' | 'high' | 'max'
export type EffortLevel = 'low' | 'medium' | 'high' | 'max' | 'xhigh'
3 changes: 2 additions & 1 deletion src/integrations/gateways/opencode-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default defineGateway({
preset: {
id: 'opencode-go',
vendorId: 'openai',
description: 'OpenCode Go — $10/mo subscription for open models (12 models)',
description: 'OpenCode Go — $10/mo subscription for open models (13 models)',
apiKeyEnvVars: ['OPENCODE_API_KEY'],
modelEnvVars: ['OPENAI_MODEL'],
},
Expand All @@ -50,6 +50,7 @@ export default defineGateway({
{ id: 'opencode-go-minimax-m2.5', apiName: 'minimax-m2.5', label: 'MiniMax M2.5', modelDescriptorId: 'opencode-go-minimax-m2.5', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'opencode-go-qwen3.6-plus', apiName: 'qwen3.6-plus', label: 'Qwen3.6 Plus', modelDescriptorId: 'opencode-go-qwen3.6-plus', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'opencode-go-qwen3.5-plus', apiName: 'qwen3.5-plus', label: 'Qwen3.5 Plus', modelDescriptorId: 'opencode-go-qwen3.5-plus', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'opencode-go-minimax-m3', apiName: 'minimax-m3', label: 'MiniMax M3', modelDescriptorId: 'opencode-go-minimax-m3', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
],
},
usage: { supported: false },
Expand Down
4 changes: 2 additions & 2 deletions src/integrations/gateways/opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,12 @@ describe('OpenCode model catalog', () => {

test('zen model count matches expected', () => {
const models = getCatalogEntriesForRoute('opencode')
expect(models.length).toBe(41)
expect(models.length).toBe(43)
})

test('go model count matches expected', () => {
const models = getCatalogEntriesForRoute('opencode-go')
expect(models.length).toBe(12)
expect(models.length).toBe(13)
})

test('all zen gpt models have modelDescriptorId', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/integrations/gateways/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default defineGateway({
preset: {
id: 'opencode',
vendorId: 'openai',
description: 'OpenCode Zen — pay-as-you-go AI gateway (41 models)',
description: 'OpenCode Zen — pay-as-you-go AI gateway (43 models)',
apiKeyEnvVars: ['OPENCODE_API_KEY'],
modelEnvVars: ['OPENAI_MODEL'],
},
Expand Down Expand Up @@ -56,6 +56,7 @@ export default defineGateway({
{ id: 'gpt-5-nano', apiName: 'gpt-5-nano', label: 'GPT 5 Nano', modelDescriptorId: 'opencode-gpt-5-nano', transportOverrides: { openaiShim: { endpointPath: '/responses' } } },
// Claude family — /zen/v1/messages
{ id: 'claude-opus-4-7', apiName: 'claude-opus-4-7', label: 'Claude Opus 4.7', modelDescriptorId: 'opencode-claude-opus-4-7', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'claude-opus-4-8', apiName: 'claude-opus-4-8', label: 'Claude Opus 4.8', modelDescriptorId: 'opencode-claude-opus-4-8', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'claude-opus-4-6', apiName: 'claude-opus-4-6', label: 'Claude Opus 4.6', modelDescriptorId: 'opencode-claude-opus-4-6', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'claude-opus-4-5', apiName: 'claude-opus-4-5', label: 'Claude Opus 4.5', modelDescriptorId: 'opencode-claude-opus-4-5', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
{ id: 'claude-opus-4-1', apiName: 'claude-opus-4-1', label: 'Claude Opus 4.1', modelDescriptorId: 'opencode-claude-opus-4-1', transportOverrides: { openaiShim: { endpointPath: '/messages' } } },
Expand All @@ -82,6 +83,7 @@ export default defineGateway({
{ id: 'big-pickle', apiName: 'big-pickle', label: 'Big Pickle', modelDescriptorId: 'opencode-big-pickle' },
{ id: 'deepseek-v4-flash-free', apiName: 'deepseek-v4-flash-free', label: 'DeepSeek V4 Flash Free', modelDescriptorId: 'opencode-deepseek-v4-flash-free' },
{ id: 'nemotron-3-super-free', apiName: 'nemotron-3-super-free', label: 'Nemotron 3 Super Free', modelDescriptorId: 'opencode-nemotron-3-super-free' },
{ id: 'mimo-v2.5-free', apiName: 'mimo-v2.5-free', label: 'MiMo V2.5 Free', modelDescriptorId: 'opencode-mimo-v2.5-free' },
],
},
usage: { supported: false },
Expand Down
4 changes: 2 additions & 2 deletions src/integrations/generated/integrationArtifacts.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export const PROVIDER_PRESET_MANIFEST = [
"routeId": "opencode-go",
"vendorId": "openai",
"gatewayId": "opencode-go",
"description": "OpenCode Go — $10/mo subscription for open models (12 models)",
"description": "OpenCode Go — $10/mo subscription for open models (13 models)",
"apiKeyEnvVars": [
"OPENCODE_API_KEY"
],
Expand All @@ -323,7 +323,7 @@ export const PROVIDER_PRESET_MANIFEST = [
"routeId": "opencode",
"vendorId": "openai",
"gatewayId": "opencode",
"description": "OpenCode Zen — pay-as-you-go AI gateway (41 models)",
"description": "OpenCode Zen — pay-as-you-go AI gateway (43 models)",
"apiKeyEnvVars": [
"OPENCODE_API_KEY"
],
Expand Down
54 changes: 54 additions & 0 deletions src/integrations/models/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,24 @@ export default [
contextWindow: 200_000,
maxOutputTokens: 65_536,
}),
defineModel({
id: 'opencode-claude-opus-4-8',
label: 'Claude Opus 4.8',

vendorId: 'openai',
classification: ['chat', 'reasoning'],
defaultModel: 'opencode-claude-opus-4-8',
capabilities: {
supportsVision: true,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsJsonMode: true,
supportsReasoning: true,
supportsPreciseTokenCount: false,
},
contextWindow: 200_000,
maxOutputTokens: 65_536,
}),
defineModel({
id: 'opencode-claude-opus-4-6',
label: 'Claude Opus 4.6',
Expand Down Expand Up @@ -752,6 +770,24 @@ export default [
contextWindow: 131_072,
maxOutputTokens: 32_768,
}),
defineModel({
id: 'opencode-mimo-v2.5-free',
label: 'MiMo V2.5 Free',

vendorId: 'openai',
classification: ['chat'],
defaultModel: 'opencode-mimo-v2.5-free',
capabilities: {
supportsVision: false,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsJsonMode: true,
supportsReasoning: false,
supportsPreciseTokenCount: false,
},
contextWindow: 131_072,
maxOutputTokens: 32_768,
}),

// ============================================================
// GO MODELS — https://opencode.ai/zen/go/v1
Expand Down Expand Up @@ -976,4 +1012,22 @@ export default [
contextWindow: 131_072,
maxOutputTokens: 32_768,
}),
defineModel({
id: 'opencode-go-minimax-m3',
label: 'MiniMax M3',

vendorId: 'openai',
classification: ['chat'],
defaultModel: 'minimax-m3',
capabilities: {
supportsVision: false,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsJsonMode: true,
supportsReasoning: false,
supportsPreciseTokenCount: false,
},
contextWindow: 131_072,
maxOutputTokens: 32_768,
}),
]
34 changes: 34 additions & 0 deletions src/services/api/openaiShim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2229,6 +2229,11 @@ class OpenAIShimMessages {

if (params.temperature !== undefined) responsesBody.temperature = params.temperature
if (params.top_p !== undefined) responsesBody.top_p = params.top_p
if (request.reasoning?.effort) {
responsesBody.reasoning_effort = request.reasoning.effort
responsesBody.reasoning_summary = 'auto'
responsesBody.include = ['reasoning.encrypted_content']
}

if (!omitResponsesTools && params.tools && params.tools.length > 0) {
const convertedTools = convertToolsToResponsesTools(
Expand Down Expand Up @@ -2279,6 +2284,31 @@ class OpenAIShimMessages {
anthropicBody.tool_choice = params.tool_choice
}

if (request.reasoning?.effort) {
// Shim receives OpenAI effort levels (xhigh) from client.ts, but
// Anthropic API expects 'max' not 'xhigh'. Convert for the effort field.
const effort = request.reasoning.effort === 'xhigh' ? 'max' : request.reasoning.effort
const modelLower = request.resolvedModel.toLowerCase()
const isAdaptive = modelLower.includes('opus-4-7') || modelLower.includes('opus-4-6') ||
modelLower.includes('opus-4-8') ||
modelLower.includes('opus-4.6') || modelLower.includes('opus-4.7') ||
modelLower.includes('opus-4.8') ||
modelLower.includes('sonnet-4-6') || modelLower.includes('sonnet-4.6')
const isOpus45 = modelLower.includes('opus-4-5') || modelLower.includes('opus-4.5')

if (isAdaptive) {
anthropicBody.thinking = { type: 'adaptive' }
anthropicBody.effort = effort
} else if (isOpus45) {
anthropicBody.effort = effort
} else if (effort === 'high' || effort === 'max') {
anthropicBody.thinking = {
type: 'enabled',
budgetTokens: effort === 'max' ? 31_999 : 16_000,
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

return anthropicBody
}

Expand Down Expand Up @@ -2372,6 +2402,10 @@ class OpenAIShimMessages {
}
if (params.temperature !== undefined) genConfig.temperature = params.temperature
if (params.top_p !== undefined) genConfig.topP = params.top_p
if (request.reasoning?.effort) {
const level = request.reasoning.effort === 'xhigh' ? 'high' : request.reasoning.effort
genConfig.thinkingConfig = { includeThoughts: true, thinkingLevel: level }
}
if (Object.keys(genConfig).length > 0) {
geminiBody.generationConfig = genConfig
}
Expand Down
88 changes: 77 additions & 11 deletions src/utils/effort.codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ test('gpt-5.3-codex-spark stays without effort controls', async () => {
expect(getAvailableEffortLevels('gpt-5.3-codex-spark')).toEqual([])
})

test('toPersistableEffort normalizes xhigh to max so it survives settings write', async () => {
test('toPersistableEffort passes xhigh through as a first-class level', async () => {
const { toPersistableEffort } = await importFreshEffortModule({
provider: 'openai',
supportsCodexReasoningEffort: true,
})

expect(toPersistableEffort('xhigh')).toBe('max')
expect(toPersistableEffort('xhigh')).toBe('xhigh')
expect(toPersistableEffort('max')).toBe('max')
expect(toPersistableEffort('high')).toBe('high')
expect(toPersistableEffort('medium')).toBe('medium')
Expand All @@ -128,12 +128,13 @@ test('standardEffortToOpenAI maps max to xhigh for shim payload', async () => {
})

expect(standardEffortToOpenAI('max')).toBe('xhigh')
expect(standardEffortToOpenAI('xhigh')).toBe('xhigh')
expect(standardEffortToOpenAI('high')).toBe('high')
expect(openAIEffortToStandard('xhigh')).toBe('max')
expect(openAIEffortToStandard('xhigh')).toBe('xhigh')
expect(openAIEffortToStandard('high')).toBe('high')
})

test('e2e: xhigh → persisted max → resolveAppliedEffort → wire xhigh on OpenAI/Codex (no high clamp)', async () => {
test('e2e: xhigh → persisted xhigh → resolveAppliedEffort → wire xhigh on OpenAI/Codex (no high clamp)', async () => {
const {
toPersistableEffort,
resolveAppliedEffort,
Expand All @@ -143,18 +144,16 @@ test('e2e: xhigh → persisted max → resolveAppliedEffort → wire xhigh on Op
supportsCodexReasoningEffort: true,
})

// Picker writes the OpenAI-shaped value; toPersistableEffort normalizes.
// Picker writes 'xhigh'; toPersistableEffort passes it through.
const persisted = toPersistableEffort('xhigh')
expect(persisted).toBe('max')
expect(persisted).toBe('xhigh')

// App state holds 'max'. Non-Opus 'max' must NOT be downgraded to 'high'
// when the model uses the OpenAI effort scheme — the shim converts back
// to 'xhigh' on the wire.
// App state holds 'xhigh'. The OpenAI-shaped 'xhigh' is sent to the API as-is.
const applied = resolveAppliedEffort('gpt-5.4', persisted)
expect(applied).toBe('max')
expect(applied).toBe('xhigh')

// Final wire value the client shim emits.
expect(standardEffortToOpenAI(applied as 'max')).toBe('xhigh')
expect(standardEffortToOpenAI(applied as 'xhigh')).toBe('xhigh')
})

test('e2e: max on non-Opus Anthropic model still clamps to high', async () => {
Expand All @@ -165,3 +164,70 @@ test('e2e: max on non-Opus Anthropic model still clamps to high', async () => {

expect(resolveAppliedEffort('claude-sonnet-4-6', 'max')).toBe('high')
})

test('modelSupportsXHighEffort: opus-4-7 and opus-4-8 are allowed; other Claude models are not', async () => {
const { modelSupportsXHighEffort } = await importFreshEffortModule({
provider: 'firstParty' as unknown as 'openai',
supportsCodexReasoningEffort: false,
})

expect(modelSupportsXHighEffort('claude-opus-4-7')).toBe(true)
expect(modelSupportsXHighEffort('claude-opus-4-8')).toBe(true)
expect(modelSupportsXHighEffort('opencode-claude-opus-4-8')).toBe(true)
expect(modelSupportsXHighEffort('claude-opus-4-6')).toBe(false)
expect(modelSupportsXHighEffort('claude-sonnet-4-6')).toBe(false)
expect(modelSupportsXHighEffort('claude-sonnet-4-5')).toBe(false)
expect(modelSupportsXHighEffort('claude-haiku-4-5')).toBe(false)
expect(modelSupportsXHighEffort('claude-3-5-haiku')).toBe(false)
})

test('xhigh does not appear in available levels for non-supporting models', async () => {
const { getAvailableEffortLevels } = await importFreshEffortModule({
provider: 'firstParty' as unknown as 'openai',
supportsCodexReasoningEffort: false,
})

// No xhigh, no max
expect(getAvailableEffortLevels('claude-sonnet-4-6')).toEqual([
'low',
'medium',
'high',
])
expect(getAvailableEffortLevels('claude-haiku-4-5')).toEqual([])

// Has xhigh AND max (opus-4-8)
const opusLevels = getAvailableEffortLevels('claude-opus-4-8')
expect(opusLevels).toEqual(['low', 'medium', 'high', 'xhigh', 'max'])
})

test('xhigh clamps to high on non-supporting models so stale settings.json values do not produce API errors', async () => {
const { resolveAppliedEffort } = await importFreshEffortModule({
provider: 'firstParty' as unknown as 'openai',
supportsCodexReasoningEffort: false,
})

// sonnet-4-6 supports effort but not xhigh — clamp
expect(resolveAppliedEffort('claude-sonnet-4-6', 'xhigh')).toBe('high')
// opus-4-8 supports xhigh — pass through
expect(resolveAppliedEffort('claude-opus-4-8', 'xhigh')).toBe('xhigh')
})

test('modelUsesOpenAIEffort: Claude/Gemini are excluded even on the openai provider (OpenCode native route)', async () => {
const { modelUsesOpenAIEffort, getAvailableEffortLevels } =
await importFreshEffortModule({
provider: 'openai',
supportsCodexReasoningEffort: true,
})

// Native Claude/Gemini on OpenCode use Anthropic/Google format, not OpenAI
expect(modelUsesOpenAIEffort('claude-opus-4-8')).toBe(false)
expect(modelUsesOpenAIEffort('claude-sonnet-4-6')).toBe(false)
expect(modelUsesOpenAIEffort('gemini-3-flash')).toBe(false)
// Real OpenAI-shaped models still classify as OpenAI
expect(modelUsesOpenAIEffort('gpt-5.4')).toBe(true)

// And the picker excludes xhigh for OpenCode Claude on openai provider
const opusLevels = getAvailableEffortLevels('claude-opus-4-8')
// Standard branch: no OPENAI_EFFORT_LEVELS, just the supported standard levels
expect(opusLevels).toEqual(['low', 'medium', 'high', 'xhigh', 'max'])
})
Loading