(cwd: string, cred: WorkspaceAiCred)
| 133 | }, |
| 134 | |
| 135 | async writeAiConfig(cwd: string, cred: WorkspaceAiCred): Promise<void> { |
| 136 | const hasProvider = !!(cred.baseUrl || cred.apiKey || cred.model); |
| 137 | if (!hasProvider) { |
| 138 | // Reset: delete the workspace opencode.json so opencode falls back to its |
| 139 | // global auth/config. No empty stub left behind. |
| 140 | await rm(join(cwd, OPENCODE_CONFIG_PATH), { force: true }); |
| 141 | return; |
| 142 | } |
| 143 | |
| 144 | const options: Record<string, string> = {}; |
| 145 | if (cred.baseUrl) options['baseURL'] = cred.baseUrl; |
| 146 | if (cred.apiKey) options['apiKey'] = cred.apiKey; |
| 147 | |
| 148 | // The @ai-sdk package opencode loads depends on the wire shape (all bundled): |
| 149 | // anthropic → @ai-sdk/anthropic, OpenAI Responses → @ai-sdk/openai, and |
| 150 | // OpenAI Chat Completions (the default — CN/local gateways) → the |
| 151 | // openai-compatible SDK. |
| 152 | const npm = cred.wireShape === 'anthropic' ? '@ai-sdk/anthropic' |
| 153 | : cred.wireShape === 'openai-responses' ? '@ai-sdk/openai' |
| 154 | : OPENCODE_SDK_NPM; |
| 155 | const provider: Record<string, unknown> = { |
| 156 | npm, |
| 157 | name: 'OpenAlice workspace provider', |
| 158 | options, |
| 159 | }; |
| 160 | if (cred.model) { |
| 161 | const model: Record<string, unknown> = { name: cred.model }; |
| 162 | const contextWindow = positiveNumber(cred.contextWindow); |
| 163 | if (contextWindow !== null) { |
| 164 | // opencode treats missing custom-model limits as 0, which disables its |
| 165 | // proactive context tracking. Supplying both fields satisfies its config |
| 166 | // schema while keeping output conservative and invisible in OpenAlice UI. |
| 167 | model['limit'] = { context: contextWindow, output: DEFAULT_OUTPUT_TOKENS }; |
| 168 | } |
| 169 | provider['models'] = { [cred.model]: model }; |
| 170 | } |
| 171 | |
| 172 | const config: Record<string, unknown> = { |
| 173 | $schema: 'https://opencode.ai/config.json', |
| 174 | provider: { [OPENCODE_PROVIDER_NAME]: provider }, |
| 175 | }; |
| 176 | // Top-level default model is "<provider>/<id>" so opencode resolves the |
| 177 | // workspace provider without a UI model picker. |
| 178 | if (cred.model) config['model'] = `${OPENCODE_PROVIDER_NAME}/${cred.model}`; |
| 179 | |
| 180 | await writeWorkspaceFile(cwd, OPENCODE_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n'); |
| 181 | }, |
| 182 | |
| 183 | async readAiConfig(cwd: string): Promise<WorkspaceAiCred | null> { |
| 184 | const raw = await readWorkspaceFile(cwd, OPENCODE_CONFIG_PATH); |
nothing calls this directly
no test coverage detected