( connection: Pick<ConnectionEffectConnection, 'providerType'>, baseUrl: string, apiKey: string, model: string, t0: number, fetchFn: ConnectionEffectFetch | undefined, timeoutMs = CONNECTION_TEST_TIMEOUT_MS, )
| 330 | } |
| 331 | |
| 332 | async function probeOpenAI( |
| 333 | connection: Pick<ConnectionEffectConnection, 'providerType'>, |
| 334 | baseUrl: string, |
| 335 | apiKey: string, |
| 336 | model: string, |
| 337 | t0: number, |
| 338 | fetchFn: ConnectionEffectFetch | undefined, |
| 339 | timeoutMs = CONNECTION_TEST_TIMEOUT_MS, |
| 340 | ): Promise<ConnectionTestResult> { |
| 341 | if (connection.providerType === 'openai-codex') { |
| 342 | // Codex Subscription credentials are ChatGPT account-scoped OAuth |
| 343 | // tokens. A live `/responses` probe is not a stable readiness test: |
| 344 | // the backend can hold or reject small synthetic requests even when |
| 345 | // the stored login is valid and the real send path has enough context. |
| 346 | // Mirror Claude OAuth and treat a resolved main-process OAuth token as |
| 347 | // the explicit connection test; actual turn failures still surface in |
| 348 | // chat with the provider error class. |
| 349 | return { ok: true, latencyMs: Date.now() - t0, modelTested: model }; |
| 350 | } |
| 351 | const r = await fetchForConnectionEffect(fetchFn, `${stripTrailing(baseUrl)}/chat/completions`, { |
| 352 | method: 'POST', |
| 353 | headers: { |
| 354 | ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}), |
| 355 | 'content-type': 'application/json', |
| 356 | }, |
| 357 | body: JSON.stringify({ |
| 358 | model, |
| 359 | max_tokens: 16, |
| 360 | messages: [{ role: 'user', content: 'Hi' }], |
| 361 | }), |
| 362 | timeoutMs, |
| 363 | }); |
| 364 | if (!r.ok) return httpFailure(r, t0); |
| 365 | if (connection.providerType === 'opencode-free') { |
| 366 | const body = await r.readJson<unknown>(); |
| 367 | if (!isOpenAIChatCompletion(body)) { |
| 368 | return { |
| 369 | ok: false, |
| 370 | errorMessage: 'OpenCode Free returned no valid chat completion', |
| 371 | errorClass: 'provider_unavailable', |
| 372 | latencyMs: Date.now() - t0, |
| 373 | modelTested: model, |
| 374 | }; |
| 375 | } |
| 376 | return { ok: true, latencyMs: Date.now() - t0, modelTested: model }; |
| 377 | } |
| 378 | await r.cancel(); |
| 379 | return { ok: true, latencyMs: Date.now() - t0, modelTested: model }; |
| 380 | } |
| 381 | |
| 382 | function isOpenAIChatCompletion(value: unknown): boolean { |
| 383 | if (!value || typeof value !== 'object') return false; |
no test coverage detected