From 2897f40211a657669c4582873841c6c817992711 Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Tue, 7 Jul 2026 19:19:51 +0800 Subject: [PATCH 1/2] feat(provider): add opencode go --- docs/features/opencode-go-provider/plan.md | 45 +++++ docs/features/opencode-go-provider/spec.md | 48 +++++ docs/features/opencode-go-provider/tasks.md | 11 ++ .../presenter/configPresenter/providers.ts | 15 ++ .../llmProviderPresenter/providerRegistry.ts | 18 +- .../providers/aiSdkProvider.ts | 55 ++++++ .../configPresenter/defaultProviders.test.ts | 13 ++ .../basicApiKeyProviders.test.ts | 181 +++++++++++++++++- 8 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 docs/features/opencode-go-provider/plan.md create mode 100644 docs/features/opencode-go-provider/spec.md create mode 100644 docs/features/opencode-go-provider/tasks.md diff --git a/docs/features/opencode-go-provider/plan.md b/docs/features/opencode-go-provider/plan.md new file mode 100644 index 000000000..f541a85ae --- /dev/null +++ b/docs/features/opencode-go-provider/plan.md @@ -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`. diff --git a/docs/features/opencode-go-provider/spec.md b/docs/features/opencode-go-provider/spec.md new file mode 100644 index 000000000..0d811b65d --- /dev/null +++ b/docs/features/opencode-go-provider/spec.md @@ -0,0 +1,48 @@ +# OpenCode Go Provider + +## 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. diff --git a/docs/features/opencode-go-provider/tasks.md b/docs/features/opencode-go-provider/tasks.md new file mode 100644 index 000000000..10e293638 --- /dev/null +++ b/docs/features/opencode-go-provider/tasks.md @@ -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. +- [ ] Open a PR targeting `dev`. diff --git a/src/main/presenter/configPresenter/providers.ts b/src/main/presenter/configPresenter/providers.ts index 29cb24118..b0d813be3 100644 --- a/src/main/presenter/configPresenter/providers.ts +++ b/src/main/presenter/configPresenter/providers.ts @@ -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', diff --git a/src/main/presenter/llmProviderPresenter/providerRegistry.ts b/src/main/presenter/llmProviderPresenter/providerRegistry.ts index a7e59734c..97fa3f1f9 100644 --- a/src/main/presenter/llmProviderPresenter/providerRegistry.ts +++ b/src/main/presenter/llmProviderPresenter/providerRegistry.ts @@ -12,6 +12,7 @@ export type AiSdkBehaviorPreset = export type AiSdkModelSourceStrategy = | 'openai' | 'openai-codex' + | 'opencode-go' | 'kimi-for-coding' | 'github' | 'together' @@ -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' @@ -417,6 +418,21 @@ const PROVIDER_ID_REGISTRY = new Map([ 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({ diff --git a/src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts b/src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts index eeb7cdfcc..4d0761a2e 100644 --- a/src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts @@ -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 = { @@ -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' @@ -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( @@ -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': { @@ -1855,6 +1878,38 @@ export class AiSdkProvider extends BaseLLMProvider { } } + private async fetchOpenCodeGoModels(): Promise { + const records = await this.fetchOpenAIModelRecords({ timeout: this.getModelFetchTimeout() }) + + return records + .filter((model): model is Record & { 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 { type NewApiModelRecord = { id?: unknown diff --git a/test/main/presenter/configPresenter/defaultProviders.test.ts b/test/main/presenter/configPresenter/defaultProviders.test.ts index 8afd81320..b39641293 100644 --- a/test/main/presenter/configPresenter/defaultProviders.test.ts +++ b/test/main/presenter/configPresenter/defaultProviders.test.ts @@ -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' + }) + }) }) }) diff --git a/test/main/presenter/llmProviderPresenter/basicApiKeyProviders.test.ts b/test/main/presenter/llmProviderPresenter/basicApiKeyProviders.test.ts index 9641f7455..5e83b7ec8 100644 --- a/test/main/presenter/llmProviderPresenter/basicApiKeyProviders.test.ts +++ b/test/main/presenter/llmProviderPresenter/basicApiKeyProviders.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { IConfigPresenter, LLM_PROVIDER } from '../../../../src/shared/presenter' import { AiSdkProvider } from '../../../../src/main/presenter/llmProviderPresenter/providers/aiSdkProvider' import { resolveAiSdkProviderDefinition } from '../../../../src/main/presenter/llmProviderPresenter/providerRegistry' @@ -8,6 +8,8 @@ const { mockGetProvider, mockRunAiSdkGenerateText } = vi.hoisted(() => ({ mockRunAiSdkGenerateText: vi.fn() })) +const originalFetch = global.fetch + vi.mock('electron', () => ({ app: { getName: vi.fn(() => 'DeepChat'), @@ -98,9 +100,14 @@ const createConfigPresenter = (): IConfigPresenter => describe('basic API-key provider registrations', () => { beforeEach(() => { vi.clearAllMocks() + global.fetch = originalFetch mockRunAiSdkGenerateText.mockResolvedValue({ content: 'ok' }) }) + afterEach(() => { + global.fetch = originalFetch + }) + it('resolves OpenAI-compatible providers through provider-db backed definitions', () => { const expectations = [ ['nvidia', 'nvidia', 'microsoft/phi-4-mini-instruct'], @@ -130,6 +137,26 @@ describe('basic API-key provider registrations', () => { } }) + it('resolves OpenCode Go through its mixed-route provider definition', () => { + expect( + resolveAiSdkProviderDefinition( + createProvider({ + id: 'opencode-go', + name: 'OpenCode Go', + baseUrl: 'https://opencode.ai/zen/go/v1' + }) + ) + ).toMatchObject({ + runtimeKind: 'openai-compatible', + modelSource: 'opencode-go', + checkStrategy: 'generate-text', + credentialStrategy: 'api-key', + routeStrategy: 'opencode-go', + embeddingStrategy: 'none', + checkModelId: 'kimi-k2.7-code' + }) + }) + it('resolves MiniMax global through the Anthropic-compatible runtime', () => { expect( resolveAiSdkProviderDefinition( @@ -151,6 +178,67 @@ describe('basic API-key provider registrations', () => { }) }) + it('maps OpenCode Go model records and marks messages models for Anthropic routing', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + object: 'list', + data: [ + { id: 'kimi-k2.7-code', object: 'model', owned_by: 'opencode' }, + { id: 'minimax-m3', object: 'model', owned_by: 'opencode' }, + { id: 'hy3-preview', object: 'model', owned_by: 'opencode' } + ] + }) + }) + global.fetch = fetchMock as unknown as typeof fetch + + const provider = new AiSdkProvider( + createProvider({ + id: 'opencode-go', + name: 'OpenCode Go', + baseUrl: 'https://opencode.ai/zen/go/v1' + }), + createConfigPresenter() + ) + const models = await provider.fetchModels() + + expect(fetchMock).toHaveBeenCalledWith( + 'https://opencode.ai/zen/go/v1/models', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + Authorization: 'Bearer test-key' + }) + }) + ) + expect(models).toEqual([ + expect.objectContaining({ + id: 'kimi-k2.7-code', + group: 'Chat Completions', + providerId: 'opencode-go', + endpointType: 'openai', + supportedEndpointTypes: ['openai'], + ownedBy: 'opencode' + }), + expect.objectContaining({ + id: 'minimax-m3', + group: 'Messages', + providerId: 'opencode-go', + endpointType: 'anthropic', + supportedEndpointTypes: ['anthropic'], + ownedBy: 'opencode' + }), + expect.objectContaining({ + id: 'hy3-preview', + group: 'Chat Completions', + providerId: 'opencode-go', + endpointType: 'openai', + supportedEndpointTypes: ['openai'], + ownedBy: 'opencode' + }) + ]) + }) + it('maps provider DB metadata into built-in provider models', async () => { mockGetProvider.mockReturnValue({ id: 'nvidia', @@ -193,6 +281,97 @@ describe('basic API-key provider registrations', () => { ]) }) + it('routes OpenCode Go chat completions models through OpenAI-compatible runtime', async () => { + const provider = new AiSdkProvider( + createProvider({ + id: 'opencode-go', + name: 'OpenCode Go', + baseUrl: 'https://opencode.ai/zen/go/v1' + }), + createConfigPresenter() + ) + ;(provider as any).isInitialized = true + + await expect(provider.check()).resolves.toEqual({ + isOk: true, + errorMsg: null + }) + expect(mockRunAiSdkGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + providerKind: 'openai-compatible', + provider: expect.objectContaining({ + id: 'opencode-go', + apiType: 'openai-completions', + baseUrl: 'https://opencode.ai/zen/go/v1' + }) + }), + [{ role: 'user', content: 'Hello' }], + 'kimi-k2.7-code', + expect.any(Object), + 0.2, + 16 + ) + }) + + it('uses Anthropic behavior for OpenCode Go messages models', async () => { + const provider = new AiSdkProvider( + createProvider({ + id: 'opencode-go', + name: 'OpenCode Go', + baseUrl: 'https://opencode.ai/zen/go/v1' + }), + createConfigPresenter() + ) + ;(provider as any).isInitialized = true + + await provider.summaryTitles( + [{ role: 'user', content: 'Explain provider routing' }], + 'minimax-m3' + ) + + expect(mockRunAiSdkGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + providerKind: 'anthropic' + }), + expect.any(Array), + 'minimax-m3', + expect.any(Object), + 0.3, + 50 + ) + }) + + it('routes OpenCode Go messages models through Anthropic runtime', async () => { + const provider = new AiSdkProvider( + createProvider({ + id: 'opencode-go', + name: 'OpenCode Go', + baseUrl: 'https://opencode.ai/zen/go/v1' + }), + createConfigPresenter() + ) + ;(provider as any).isInitialized = true + + await provider.runText([{ role: 'user', content: 'Hello' }], 'minimax-m3') + + expect(mockRunAiSdkGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + providerKind: 'anthropic', + provider: expect.objectContaining({ + id: 'opencode-go', + apiType: 'anthropic', + baseUrl: 'https://opencode.ai/zen/go/v1', + capabilityProviderId: 'anthropic' + }) + }), + [{ role: 'user', content: 'Hello' }], + 'minimax-m3', + expect.any(Object), + undefined, + undefined + ) + }) + it('uses the configured check model for MiniMax global', async () => { const provider = new AiSdkProvider( createProvider({ From 352c1ce24a9cd4005ad66555a7caaf2fed60a54f Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Tue, 7 Jul 2026 19:21:53 +0800 Subject: [PATCH 2/2] docs(provider): update opencode go sdd --- docs/features/opencode-go-provider/spec.md | 2 ++ docs/features/opencode-go-provider/tasks.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/features/opencode-go-provider/spec.md b/docs/features/opencode-go-provider/spec.md index 0d811b65d..0b931d1a2 100644 --- a/docs/features/opencode-go-provider/spec.md +++ b/docs/features/opencode-go-provider/spec.md @@ -1,5 +1,7 @@ # 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. diff --git a/docs/features/opencode-go-provider/tasks.md b/docs/features/opencode-go-provider/tasks.md index 10e293638..d2066a38a 100644 --- a/docs/features/opencode-go-provider/tasks.md +++ b/docs/features/opencode-go-provider/tasks.md @@ -8,4 +8,4 @@ - [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. -- [ ] Open a PR targeting `dev`. +- [x] Open a PR targeting `dev`.