feat(llm): add Cohere chat adapter (command-r / command-r-plus)#13
feat(llm): add Cohere chat adapter (command-r / command-r-plus)#13mvanhorn wants to merge 1 commit into
Conversation
Adds command-r and command-r-plus support via Cohere's Chat API, matching the issue jstuart0#7 brief. Cohere uses a different wire format than OpenAI's chat completions: Request: { message, preamble, chat_history? } Response: { text, meta: { tokens: {...} } } Error: { message: '...' } (no nested error object) The adapter maps systemPrompt -> preamble and transcriptPrompt -> message, which matches how the watcher uses the existing adapters (single system + single user turn per call). chat_history is left empty because the watcher always sends a fresh transcript excerpt. Pricing rows added for command-r (15c/60c per 1M input/output) and command-r-plus (250c/1000c per 1M), placing them between Gemini Flash and OpenAI gpt-4o respectively. list-models gains a Cohere branch that hits /v1/models with a Bearer Authorization header. The existing { models: [...] } fallback in the parser already handles Cohere's response shape, so the listing branch is small. Test plan: - bun test src/server/services/ai/llm/llm.test.ts -- 20 pass (5 new cohere adapter cases + 2 new pricing cases) - bun run typecheck -- 0 errors - bunx biome check src/server/services/ai/llm/ -- 0 errors Closes jstuart0#7
A code-health audit found 22 instances of string-literal-union types duplicated server↔client (or paired with hand-maintained runtime allowlists) with no compile-time enforcement. The recent Cohere PR (#13) exposed this: it added a ProviderKind value to the type but missed three other places where the kind list lived independently. This slice closes 10 of the highest-leverage simple cases using one uniform recipe: export const KIND_NAMES = ["a", "b", "c"] as const; export type KindName = (typeof KIND_NAMES)[number]; so adding a kind requires updating exactly one declaration and tsc chases every consumer. Fixes: 1. ProviderKind — canonical KNOWN_PROVIDER_KINDS in shared/types.ts. 5 sites consolidated; AiSettingsPanel UI dropdown gains an exhaustiveness assertion via `as const satisfies`. Pricing FREE_KINDS, route allowlist, and client AiProviderKind alias all re-export. 2. AgentType — derived from AGENT_TYPES tuple in shared/constants. AGENT_TYPE_LABELS retyped to Record<AgentType, string>. routes/projects.ts and template-preview.ts drop hand-maintained allowlists in favor of AGENT_TYPES.includes(). 3. SessionStatus — derived from SESSION_STATUSES; STATUS_COLORS: Record<SessionStatus, string>. 4. SemanticStatus — derived from SEMANTIC_STATUSES; SEMANTIC_STATUS_COLORS: Record<SemanticStatus, string>. 5. ApprovalPolicy + SandboxMode — APPROVAL_POLICIES / SANDBOX_MODES `as const` tuples; templates/utils.ts dropdowns spread the tuples. 6. AskMessageRole — single shared type; ask-service + api.ts both import. 7. WatcherPolicy — single shared type; watcher-config-service + api re-export. 8. DecisionKind — single shared type; 2-member subsets at InboxWorkItem.decision and SendChannelMessageInput.decision now Extract<DecisionKind, "continue" | "ask"> so removing either from the parent union breaks compile. 9. HitlReplyKind — single shared HITL_REPLY_KINDS; 6 inline literals replaced. 10. ActionRequestKind — converted from asserted-array to derived type. Cross-validation with ActionRequestPayload["kind"] union; removing an entry cascades errors through inbox-service and ask-bulk-action-handler. Real bug exposed: AgentTypeBadge.tsx was indexing AGENT_TYPE_LABELS with an arbitrary string prop. The tightened Record<AgentType, …> rejected the access; an explicit fallback comment + cast documents that legacy data may carry unknown values. Tests: - New shared/types.test.ts (7 tests): asserts every member of each KNOWN_*_KINDS const is type-compatible and the runtime check rejects bogus values. - Existing tests untouched (594/594 pass). Sanity-checked the exhaustiveness guards by temporarily removing entries — confirmed tsc fires for ProviderKind, ActionRequestKind, and AgentType. 587 → 594 tests pass; typecheck clean; biome 0 errors.
Audit findings #14, #17, #18, #19, #13, #22 — final cleanup of the type-duplication audit. Every finding from the original audit is now addressed across TYPE-2a/b/c/d. Fix 1 — EventCategory exhaustiveness (#17, High): - TimelineView.tsx eventLabel: replaced default: "Event" with explicit cases for all 7 ai_* categories + a never exhaustive guard. The ai_* events previously rendered with an anonymous "Event" label; now identify themselves: "AI Proposal", "AI HITL", "AI Continue", "AI Report", etc. - ai/context.ts renderEventLine: same treatment. The classifier context was dropping AI events entirely (default: ""); now each contributes a one-liner like "[ai] proposal: continue". Fix 2 — AskThreadOrigin (#14): - Hoisted ASK_THREAD_ORIGINS as const + AskThreadOrigin to src/shared/types.ts. Replaced the inline "web" | "telegram" literal at 44 sites across 14 files. Fix 3 — ActionRequestDecision (#19): - Hoisted ACTION_REQUEST_DECISIONS + type to shared. 12 inbox cards + InboxPage + api.ts + routes/ai.ts + 2 service compare sites collapsed. Fix 4 — WatcherRunTriggerKind (#13): - Server-only type; context.ts now imports from watcher-runs-service.ts, dropping the 5-member inline duplicate. Fix 5 — InboxKind Extract (#18): - inbox-snooze-service.ts InboxKind now Extract<InboxWorkItem["kind"], "hitl" | "stuck" | "risky" | "failed_proposal">. Renaming any of those four kinds in InboxWorkItem now cascades to the snooze service immediately. Fix 6 — LabsFlag (#22): - Hoisted KNOWN_LABS_FLAGS + LabsFlag + LabsFlags to shared. Both labs-service.ts and web/lib/api.ts re-export. Compile-time assertion that LABS_REGISTRY covers every flag. Behavior change worth flagging: AI events in the session timeline were previously labeled "Event" and dropped from classifier context; now have explicit labels and one-line context entries. Visible improvement (AI events were anonymous), not a regression. Sanity checks (both passed): - Removing case "ai_proposal" → tsc fires '"ai_proposal"' is not assignable to type 'never'. Restored. - Renaming kind: "hitl" in InboxWorkItem → tsc cascades to snooze-service Extract + 5 other sites. Restored. Tests: - shared/types.test.ts (+3 allowlist membership tests) 609 → 612 tests pass; typecheck clean; biome 0 errors. This closes the type-duplication audit. All 22 findings collapsed across TYPE-2a/b/c/d. Refs evaluation-report.md type-duplication audit #13, #14, #17, #18, #19, #22.
|
Thanks for this — really appreciate the work, and apologies for the timing churn. After you opened this, we landed a refactor on Net effect: your diff gets smaller after a rebase. Concrete checklist for
You can drop the changes to Once rebased, all your existing 7 tests stay green, plus Happy to review the rebase when you push it, or take it from here ourselves if you'd rather not deal with the churn — just say the word. Either way, thanks again. |
What
Adds a Cohere Chat API adapter so users can route the watcher to command-r / command-r-plus instead of being limited to Anthropic + the OpenAI-compatible surface.
Cohere's wire format differs from OpenAI's:
{ message, preamble, chat_history? }{ text, meta: { tokens: { input_tokens, output_tokens } } }{ message: \"...\" }(flat, no nested error object)The adapter maps
systemPrompt->preambleandtranscriptPrompt->message, matching how the watcher uses every other adapter (single system + single user turn per call).chat_historyis omitted because the watcher always sends a fresh transcript excerpt; multi-turn history is the caller's responsibility.Files
cohere.ts(new): adapter using/v1/chat, Bearer auth, classifyHttpError, and a Cohere-specific error message extractortypes.ts:\"cohere\"added toProviderKindregistry.ts:getAdapterrouteskind === \"cohere\"tocreateCohereAdapter;defaultBaseUrlreturnshttps://api.cohere.compricing.ts: adds command-r (15/60c per 1M) and command-r-plus (250/1000c per 1M)list-models.ts: adds a Cohere branch hitting/v1/modelswith Bearer auth (the existing.modelsfallback in the parser already handles Cohere's response shape)llm.test.ts: 5 new adapter tests + 2 pricing testsTest plan
bun test src/server/services/ai/llm/llm.test.ts: 20 pass / 0 fail (52 expect() calls, was 13 / 0 before)bun run typecheck: 0 errors (the new ProviderKind exhausted a previously-incomplete switch inlist-models.ts; that's now fixed)bunx biome check src/server/services/ai/llm/: 0 errorsNotes
completeStreamis intentionally not implemented in this PR. Cohere streams via SSE-styleevent: text-generationchunks; the existingstreamWithFallbackwrapper intypes.tswill serve full responses as a single delta until that's added in a follow-up.extractCohereErrorMessageparses the{ message }shape and falls back to the raw body on parse failure, so opaque error responses still produce usefulLlmError.messagetext.Closes #7