Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions docs/features/opencode-go-provider/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Plan

## Provider Path

Use the existing AI SDK provider infrastructure with a small provider-specific registry definition:

- Runtime family: mixed OpenAI-compatible plus Anthropic route selection.
- Default route: OpenAI-compatible `/chat/completions`.
- Anthropic route: selected for documented `/messages` model IDs.
- Auth: existing API-key field, sent as bearer key by the selected AI SDK transport.
- Model metadata source: live OpenCode Go `/models` endpoint.

## Affected Files

- `src/main/presenter/configPresenter/providers.ts`
- Add the built-in `opencode-go` provider entry.
- `src/main/presenter/llmProviderPresenter/providerRegistry.ts`
- Add Go-specific model source and route strategy values.
- Register `opencode-go` with `checkStrategy: generate-text` and `checkModelId: kimi-k2.7-code`.
- `src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts`
- Add OpenCode Go model endpoint mapping.
- Route documented `/messages` model IDs to `providerKind: 'anthropic'`.
- Focused tests under `test/main/**`.

## Data Flow

1. User enables `opencode-go` and saves the OpenCode Go API key.
2. Model refresh calls `GET https://opencode.ai/zen/go/v1/models` with the provider API key.
3. Models documented for `/messages` receive `endpointType: 'anthropic'`; other Go chat models receive `endpointType: 'openai'`.
4. Runtime route selection patches Anthropic models to `apiType: 'anthropic'` with the same OpenCode Go base URL.
5. OpenAI-compatible models use the default OpenAI-compatible runtime against `/chat/completions`.

## Compatibility

- Existing OpenAI-compatible and Anthropic providers are unchanged.
- OpenCode Go does not depend on PublicProviderConf refresh availability.
- Users can unlock and edit the base URL through the existing built-in provider UI if needed.

## Test Strategy

- Unit test default provider metadata.
- Unit test registry definition and connection-check model.
- Unit test model mapping from mocked `/models` payload.
- Unit test route selection by invoking `check()` for OpenAI and Anthropic Go model IDs with the AI SDK generate-text runner mocked.
- Run focused tests, then `pnpm run format`, `pnpm run i18n`, and `pnpm run lint`.
50 changes: 50 additions & 0 deletions docs/features/opencode-go-provider/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# OpenCode Go Provider

Linked GitHub issue: #1896

## User Need

DeepChat users with an OpenCode Go subscription need to configure their Go API key and use the Go model catalog from DeepChat as a built-in provider.

## Goal

Add OpenCode Go as a built-in DeepChat provider using the documented OpenCode Go endpoints at `https://opencode.ai/zen/go/v1`.

## Requirements

- Add a disabled built-in provider with:
- provider ID: `opencode-go`
- display name: `OpenCode Go`
- auth: API key copied from OpenCode Zen
- default base URL: `https://opencode.ai/zen/go/v1`
- Use the official documentation URLs:
- official/API key console: `https://opencode.ai/auth`
- docs: `https://opencode.ai/docs/zh-cn/go/`
- models endpoint: `https://opencode.ai/zen/go/v1/models`
- Fetch available models from `GET /models` and map OpenCode Go model IDs into DeepChat `MODEL_META` records.
- Route models documented for `https://opencode.ai/zen/go/v1/chat/completions` through the OpenAI-compatible runtime.
- Route models documented for `https://opencode.ai/zen/go/v1/messages` through the existing Anthropic runtime.
- Use `kimi-k2.7-code` as the connection-check model because it is a documented OpenAI-compatible Go coding model.
- Keep credentials in the existing provider API-key field; do not add OAuth or renderer-side credential storage.
- Preserve proxy support and existing AiSdkProvider behaviors.

## Acceptance Criteria

- `DEFAULT_PROVIDERS` includes `opencode-go` as disabled with the documented base URL and websites.
- `resolveAiSdkProviderDefinition` resolves `opencode-go` to an AI SDK provider definition with a Go-specific model source and route strategy.
- Fetching models maps OpenCode Go `/models` payloads to DeepChat models and marks documented `/messages` models for Anthropic routing.
- OpenCode Go Anthropic-routed model IDs build runtime context with `providerKind: 'anthropic'` and OpenCode Go base URL.
- OpenCode Go OpenAI-compatible model IDs build runtime context with `providerKind: 'openai-compatible'`.
- Focused tests cover provider registration, model mapping, route selection, and check-model configuration.
- SDD files contain no unresolved `[NEEDS CLARIFICATION]` markers.

## Non-Goals

- No new SDK dependency.
- No special OAuth flow.
- No PublicProviderConf/provider-db metadata dependency for OpenCode Go in this change.
- No support for endpoints outside the documented chat/completions, messages, and models routes.

## Open Questions

None.
11 changes: 11 additions & 0 deletions docs/features/opencode-go-provider/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Tasks

- [x] Read OpenCode Go documentation and derive provider details.
- [x] Inspect existing DeepChat provider architecture and test conventions.
- [x] Add built-in OpenCode Go provider metadata.
- [x] Add OpenCode Go registry definition and model/route strategies.
- [x] Add OpenCode Go model mapping and route selection.
- [x] Add focused tests for registration, model mapping, routing, and check strategy.
- [x] Run formatting, i18n, lint, and focused tests.
- [x] Self-review the diff, fix findings, and repeat review until clean.
- [x] Open a PR targeting `dev`.
15 changes: 15 additions & 0 deletions src/main/presenter/configPresenter/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,21 @@ export const DEFAULT_PROVIDERS: LLM_PROVIDER_BASE[] = [
defaultBaseUrl: 'https://openrouter.ai/api/v1/'
}
},
{
id: 'opencode-go',
name: 'OpenCode Go',
apiType: 'openai-completions',
apiKey: '',
baseUrl: 'https://opencode.ai/zen/go/v1',
enable: false,
websites: {
official: 'https://opencode.ai/auth',
apiKey: 'https://opencode.ai/auth',
docs: 'https://opencode.ai/docs/zh-cn/go/',
models: 'https://opencode.ai/zen/go/v1/models',
defaultBaseUrl: 'https://opencode.ai/zen/go/v1'
}
},
{
id: 'poe',
name: 'Poe',
Expand Down
18 changes: 17 additions & 1 deletion src/main/presenter/llmProviderPresenter/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type AiSdkBehaviorPreset =
export type AiSdkModelSourceStrategy =
| 'openai'
| 'openai-codex'
| 'opencode-go'
| 'kimi-for-coding'
| 'github'
| 'together'
Expand Down Expand Up @@ -41,7 +42,7 @@ export type AiSdkCheckStrategy = 'fetch-models' | 'key-status' | 'generate-text'

export type AiSdkCredentialStrategy = 'none' | 'api-key' | 'anthropic' | 'vertex' | 'bedrock'

export type AiSdkRouteStrategy = 'none' | 'grok' | 'new-api' | 'zenmux'
export type AiSdkRouteStrategy = 'none' | 'grok' | 'new-api' | 'opencode-go' | 'zenmux'

export type AiSdkEmbeddingStrategy = 'none' | 'openai' | 'google' | 'new-api' | 'zenmux'

Expand Down Expand Up @@ -417,6 +418,21 @@ const PROVIDER_ID_REGISTRY = new Map<string, AiSdkProviderDefinition>([
providerDbGroup: 'o3fan'
})
],
[
'opencode-go',
createDefinition({
...OPENAI_BASE,
modelSource: 'opencode-go',
checkStrategy: 'generate-text',
credentialStrategy: 'api-key',
routeStrategy: 'opencode-go',
embeddingStrategy: 'none',
checkModelId: 'kimi-k2.7-code',
checkPrompt: 'Hello',
checkTemperature: 0.2,
checkMaxTokens: 16
})
],
[
'openai',
createDefinition({
Expand Down
55 changes: 55 additions & 0 deletions src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ const OPENAI_CODEX_RECOMMENDED_MODEL_IDS = [
'gpt-5.4-mini',
'gpt-5.3-codex-spark'
]
// Keep this aligned with the OpenCode Go docs table for models served by /messages.
const OPENCODE_GO_ANTHROPIC_MODEL_IDS = new Set([
'minimax-m3',
'minimax-m2.7',
'minimax-m2.5',
'qwen3.7-max',
'qwen3.7-plus',
'qwen3.6-plus'
])
const DEFAULT_NEW_API_BASE_URL = 'https://www.newapi.ai'

type RouteDecision = {
Expand Down Expand Up @@ -309,6 +318,7 @@ export class AiSdkProvider extends BaseLLMProvider {
private getBehaviorPreset(decision: RouteDecision): AiSdkBehaviorPreset {
switch (this.getRouteStrategy()) {
case 'new-api':
case 'opencode-go':
case 'zenmux':
if (decision.providerKind === 'anthropic' || decision.providerKind === 'aws-bedrock') {
return 'anthropic'
Expand Down Expand Up @@ -468,6 +478,17 @@ export class AiSdkProvider extends BaseLLMProvider {
}
}

if (strategy === 'opencode-go' && OPENCODE_GO_ANTHROPIC_MODEL_IDS.has(modelId)) {
return {
providerKind: 'anthropic',
providerPatch: {
apiType: 'anthropic',
baseUrl: this.provider.baseUrl,
capabilityProviderId: 'anthropic'
}
}
}

if (strategy === 'new-api') {
const endpointType = this.resolveNewApiEndpointType(modelId)
const capabilityProviderId = this.resolveNewApiRuntimeCapabilityProviderId(
Expand Down Expand Up @@ -1508,6 +1529,8 @@ export class AiSdkProvider extends BaseLLMProvider {
return this.mapProviderDbModels(this.definition.providerDbGroup || 'default')
case 'openai-codex':
return this.mapOpenAICodexModels()
case 'opencode-go':
return this.fetchOpenCodeGoModels()
case 'kimi-for-coding':
return this.mapKimiForCodingModels()
case 'github': {
Expand Down Expand Up @@ -1855,6 +1878,38 @@ export class AiSdkProvider extends BaseLLMProvider {
}
}

private async fetchOpenCodeGoModels(): Promise<MODEL_META[]> {
const records = await this.fetchOpenAIModelRecords({ timeout: this.getModelFetchTimeout() })

return records
.filter((model): model is Record<string, unknown> & { id: string } => {
return typeof model.id === 'string' && model.id.trim().length > 0
})
.map((model) => {
const modelId = model.id.trim()
const isAnthropicModel = OPENCODE_GO_ANTHROPIC_MODEL_IDS.has(modelId)
const existingConfig = this.getProviderModelConfig(modelId)
const endpointType = isAnthropicModel ? 'anthropic' : 'openai'

return {
id: modelId,
name: modelId,
group: isAnthropicModel ? 'Messages' : 'Chat Completions',
providerId: this.provider.id,
isCustom: false,
endpointType,
supportedEndpointTypes: [endpointType],
ownedBy: typeof model.owned_by === 'string' ? model.owned_by : 'opencode',
contextLength: existingConfig.contextLength || DEFAULT_MODEL_CONTEXT_LENGTH,
maxTokens: existingConfig.maxTokens || DEFAULT_MODEL_MAX_TOKENS,
vision: existingConfig.vision || false,
functionCall: existingConfig.functionCall || false,
reasoning: existingConfig.reasoning || false,
type: ModelType.Chat
} satisfies MODEL_META
})
}

private async fetchNewApiModels(): Promise<MODEL_META[]> {
type NewApiModelRecord = {
id?: unknown
Expand Down
13 changes: 13 additions & 0 deletions test/main/presenter/configPresenter/defaultProviders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,18 @@ describe('DEFAULT_PROVIDERS', () => {
baseUrl: 'https://api.minimax.io/anthropic/v1',
enable: false
})
expect(providersById.get('opencode-go')).toMatchObject({
name: 'OpenCode Go',
apiType: 'openai-completions',
baseUrl: 'https://opencode.ai/zen/go/v1',
enable: false,
websites: expect.objectContaining({
official: 'https://opencode.ai/auth',
apiKey: 'https://opencode.ai/auth',
docs: 'https://opencode.ai/docs/zh-cn/go/',
models: 'https://opencode.ai/zen/go/v1/models',
defaultBaseUrl: 'https://opencode.ai/zen/go/v1'
})
})
})
})
Loading