import type { ChatMessage, ChatCompletionResponse, ChatCompletionChunk, } from '@freellmapi/shared/types.js'; import { BaseProvider, providerHttpError, type CompletionOptions } from './base.js'; import { flattenMessageContent } from '../lib/content.js'; const API_BASE = 'https://api.cohere.ai/compatibility/v1'; export class CohereProvider extends BaseProvider { readonly platform = 'cohere' as const; readonly name = 'Cohere'; async chatCompletion( apiKey: string, messages: ChatMessage[], modelId: string, options?: CompletionOptions, ): Promise { const body: Record = { model: modelId, messages: flattenMessageContent(messages), temperature: options?.temperature, max_tokens: options?.max_tokens, top_p: options?.top_p, tools: options?.tools, tool_choice: options?.tool_choice, }; const res = await this.fetchWithTimeout(`${API_BASE}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw providerHttpError(res, `Cohere API error ${res.status}: ${(err as any).error?.message ?? res.statusText}`); } const data = await res.json() as ChatCompletionResponse; data._routed_via = { platform: 'cohere', model: modelId }; return data; } async *streamChatCompletion( apiKey: string, messages: ChatMessage[], modelId: string, options?: CompletionOptions, ): AsyncGenerator { const body: Record = { model: modelId, messages: flattenMessageContent(messages), temperature: options?.temperature, max_tokens: options?.max_tokens, top_p: options?.top_p, tools: options?.tools, tool_choice: options?.tool_choice, stream: true, }; const res = await this.fetchWithTimeout(`${API_BASE}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw providerHttpError(res, `Cohere API error ${res.status}: ${(err as any).error?.message ?? res.statusText}`); } yield* this.readSseStream(res); } async validateKey(apiKey: string): Promise { // Transport errors propagate — health.ts marks status='error' without // counting toward auto-disable. Only confirmed 401/403 disables a key. const res = await this.fetchWithTimeout(`${API_BASE}/models`, { method: 'GET', headers: { 'Authorization': `Bearer ${apiKey}` }, }, 10000); return res.status !== 401 && res.status !== 403; } }