import type { ChatMessage, ChatCompletionResponse, ChatCompletionChunk, ChatToolDefinition, } from '@freellmapi/shared/types.js'; import { BaseProvider, providerHttpError, type CompletionOptions } from './base.js'; import { flattenMessageContent } from '../lib/content.js'; import { recordQuotaObservationsFromResponse, type QuotaObservationContext } from '../services/provider-quota.js'; import { stripSchemaKeys } from '../lib/tool-args.js'; const API_BASE = 'https://api.cohere.ai/compatibility/v1'; // Cohere's compat-endpoint tool-schema validator rejects a couple of JSON-Schema // keywords that strict clients (opencode, continue.dev) send by default, 400-ing // the whole request and silently killing tool calls. They carry no meaning for // the provider, so strip them before sending. Mirrors google.ts's sanitizeForGemini // but scoped to the keys Cohere actually rejects, since its endpoint is otherwise // OpenAI-compatible. (Multi-fork-validated: SeanPedersen, andersmmg, chirag127.) const COHERE_UNSUPPORTED_SCHEMA_KEYS = new Set(['additionalProperties', '$schema']); function sanitizeCohereTools(tools?: ChatToolDefinition[]): ChatToolDefinition[] | undefined { if (!tools || tools.length === 0) return tools; return tools.map((t) => t.function?.parameters ? { ...t, function: { ...t.function, parameters: stripSchemaKeys(t.function.parameters, COHERE_UNSUPPORTED_SCHEMA_KEYS) } } : t, ); } export class CohereProvider extends BaseProvider { readonly platform = 'cohere' as const; readonly name = 'Cohere'; async chatCompletion( apiKey: string, messages: ChatMessage[], modelId: string, options?: CompletionOptions, quotaContext?: QuotaObservationContext, ): Promise { const body: Record = { model: modelId, messages: flattenMessageContent(messages), temperature: options?.temperature, max_tokens: options?.max_tokens, top_p: options?.top_p, tools: sanitizeCohereTools(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), }); recordQuotaObservationsFromResponse(res, { platform: this.platform, keyId: quotaContext?.keyId, providerAccountId: quotaContext?.providerAccountId, modelId, quotaPoolKey: quotaContext?.quotaPoolKey, endpoint: 'chat/completions', }); 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, quotaContext?: QuotaObservationContext, ): AsyncGenerator { const body: Record = { model: modelId, messages: flattenMessageContent(messages), temperature: options?.temperature, max_tokens: options?.max_tokens, top_p: options?.top_p, tools: sanitizeCohereTools(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), }); recordQuotaObservationsFromResponse(res, { platform: this.platform, keyId: quotaContext?.keyId, providerAccountId: quotaContext?.providerAccountId, modelId, quotaPoolKey: quotaContext?.quotaPoolKey, endpoint: 'chat/completions', }); 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, quotaContext?: QuotaObservationContext): 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); recordQuotaObservationsFromResponse(res, { platform: this.platform, keyId: quotaContext?.keyId, providerAccountId: quotaContext?.providerAccountId, modelId: quotaContext?.modelId, quotaPoolKey: quotaContext?.quotaPoolKey, endpoint: 'models', }); return res.status !== 401 && res.status !== 403; } }