Skip to content

feat(ann-16): provider plugins — OpenAI, OpenAICompatible, Copilot, Paperclip#38

Merged
kurtstohrer merged 5 commits into
mainfrom
feat/ann-16-provider-plugins
May 12, 2026
Merged

feat(ann-16): provider plugins — OpenAI, OpenAICompatible, Copilot, Paperclip#38
kurtstohrer merged 5 commits into
mainfrom
feat/ann-16-provider-plugins

Conversation

@kurtstohrer

Copy link
Copy Markdown
Owner

Summary

  • Adds four new LLMProvider implementations behind the existing interface in src/embedded/provider.ts: OpenAIProvider, OpenAICompatibleProvider, CopilotProvider, PaperclipProvider.
  • Extracts a shared chat-completions.ts (request builder + SSE parser + transport abstraction) so OpenAI / opencode / Ollama / LM Studio / vLLM / Copilot / Paperclip-hosted endpoints all share one parser instead of one per provider.
  • Polishes AnthropicProvider: new cacheTools option, omits is_error when not set (Anthropic rejects undefined), skips zero-length text deltas that some tool-loop turns emit.
  • CopilotProvider ports the sst/opencode auth flow: parses ~/.config/github-copilot/apps.json or hosts.json, exchanges the GitHub OAuth token for a Copilot session token, caches it until its expires_at window, then calls the Copilot chat endpoint with Editor-Version / Copilot-Integration-Id headers.

Exit criteria for ANN-16

  • Each provider passes a shared contract test (provider-contract.test.ts) — same scripted turn drives all five providers and asserts the same text → tool_call → usage → done event-order invariants.
  • All providers conform to the existing LLMProvider interface — no caller changes needed.
  • On-demand Copilot smoke test (copilot-smoke.test.ts, gated on ANNOTASK_COPILOT_SMOKE=1) that hits the real GitHub session-token + Copilot chat endpoints so we catch contract drift before users do.

Test plan

  • pnpm vitest run src/embedded/__tests__/ — 65 passed, 2 skipped (the gated Copilot smoke).
  • On a Copilot-signed-in machine: ANNOTASK_COPILOT_SMOKE=1 pnpm vitest run src/embedded/__tests__/copilot-smoke.test.ts (run on demand, not in CI).
  • Hand-verify against opencode local endpoint once the embedded chat UI lands (separate ticket).

Refs: ANN-8 plan, ANN-16.

kurtstohrer and others added 5 commits May 12, 2026 16:17
Foundation for the embedded agent (ANN-3). The MCP server and the (future)
embedded runner now share a single source of truth for skill content.

- src/skills: cached loader for bundled SKILL.md + companion playbooks, plus
  getSystemPrompt() that assembles the prompt with the task-type companion
  appended (A11Y_RULES.md for a11y_fix, THEME_UPDATE.md for theme_update).
- src/embedded/provider.ts: LLMProvider interface + ProviderEvent union
  (text / tool_call / usage / done / error). Stream contract is minimal so
  OpenAI / Gemini implementations can land later without touching callers.
- src/embedded/anthropic-provider.ts: concrete provider using the official
  Anthropic SDK with cache_control: ephemeral on the system block. SDK is a
  peer-optional dep and loaded lazily so MCP-only consumers don't carry it.
- src/mcp/server.ts: initialize response now returns the annotask-apply
  system prompt as `instructions`, sourced from the shared loader.
- Tests: skill loader idempotency + companion selection, provider stream
  ordering with a fake SDK client, cache-control marker placement, and the
  MCP initialize wiring (raw HTTP round-trip).

265/265 vitest pass; tsc --noEmit clean; tsup build succeeds.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Toolbar previously mixed raw glyph buttons (⟳ Scan page, ⌨ Tab order) with
Lucide-icon buttons elsewhere, ran three different border-radii (4/5/6px),
three different paddings (3px 8px, 3px 10px, 4px 10px), and had a divider
bug where panel-toggle segments with 3+ buttons lost dividers between
middle pills. Inputs used outline:none with no replacement focus ring,
hurting keyboard accessibility and axe-core compliance.

- Replace ⟳ / ✕ / ⌨ raw glyphs with refresh-cw / x / keyboard Lucide icons
- Unify all toolbar controls to 26px height and 6px border-radius
- Fix .panel-toggle divider bug for 3+ segments (was first-child only)
- Add focus-visible ring (--focus-ring + --accent-muted glow) to route-input,
  class-editor, new-task-toggle, and a shared toolbar focus selector
- Bump toolbar height 40→44px for better breathing room
- Bump tab-badge / toggle-badge font 8-9px → 10px, height 14 → 16px,
  add tabular-nums for stable counters (better legibility + axe small-text)
- Add prefers-reduced-motion media query that disables spin/pulse animations
  and transitions
- Add .icon-spin keyframe and spinning refresh icon for in-flight scan state

No new one-off CSS values introduced — all colors flow through the existing
63-CSS-variable theme tokens. Two icon-registry entries added (keyboard,
refresh-cw) since they were genuinely missing.

ANN-7 first-impression pass.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
ANN-10: tighten the annotask-apply skill so the first agent attempt at the
Hero CTA color-contrast violation lands a token-level edit (--accent in
tokens.css), not an inline override. Adds an e2e readiness guard that
verifies the violation still exists on default playground state and that
darkening --accent at the token definition clears axe-core.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
- docs/media/hero.png — real product composite captured against
  pnpm dev:react-vite + /__annotask/. Left pane: annotate moment
  (pin on the playground's hero, task-creation form with full element +
  source context). Right pane: task detail modal in the `applied` state
  with the real resolution note. Composed at 1760x880 over an annotask
  brand background.
- docs/media/demo.gif — 9.5s, 880x495, 1.7 MB. Real round trip on
  React + Vite: switch to pin mode, hover the "See how it works" link,
  drop the pin, file the task with file path + viewport + color scheme,
  agent state transitions (pending -> in_progress -> applied -> review)
  with the link contrast visibly raising during the applied step via
  Vite HMR, resolution note + accept/deny in the panel.
- docs/index.html — updated #demo alt + caption so the inline GIF is
  the demo until the 90-second hosted video lands.

Captures were driven by Playwright against the actual server; no
synthetic UI, no source polish for camera. The HMR override used in
the recording was reverted post-capture.

90-second hosted video (acceptance bullet 3) is in flight as ANN-9
follow-up; hosting target needs CEO confirmation before publishing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…aperclip

Implements the M1 milestone for the embedded-agent provider abstraction:

- chat-completions.ts: shared OpenAI-style request builder and streaming SSE
  parser; ChatCompletionsProvider base class + fetch transport.
- openai-provider.ts: BYOK OpenAI Chat Completions provider, gpt-4o-mini
  default, optional organization/project headers.
- openai-compatible-provider.ts: configurable base URL + key for opencode,
  Ollama, LM Studio, vLLM, llama.cpp.
- copilot-provider.ts: ports the sst/opencode auth flow — reads
  apps.json/hosts.json, exchanges the GitHub OAuth token for a Copilot
  session token, caches the token until its expires_at near-window, then
  hits the chat endpoint with Editor-Version / Copilot-Integration-Id
  headers.
- paperclip-provider.ts: POSTs to a configured Paperclip-hosted endpoint
  that speaks the Chat Completions wire format.

Anthropic polish:
- Add `cacheTools` StreamOption + cache_control on the last tool block.
- Omit `is_error` when the caller didn't flag the tool result as an error.
- Skip zero-length text deltas that some tool-loop turns emit.

Tests:
- chat-completions.test.ts: request shape, SSE chunk-boundary handling,
  abort, malformed tool args, tool/role splitting.
- openai-provider.test.ts: defaults, HTTP error surfacing, BYOK guard.
- copilot-provider.test.ts: hosts.json/apps.json reader; session-token
  cache reuse + near-expiry refresh; default fetch transport headers.
- provider-contract.test.ts: parameterized over Anthropic + OpenAI +
  OpenAICompatible + Copilot + Paperclip — same scripted turn, same
  event-order invariants.
- copilot-smoke.test.ts: gated on ANNOTASK_COPILOT_SMOKE=1; exercises real
  GitHub session-token endpoint + Copilot chat endpoint so we catch
  contract drift on demand.

All callers continue to depend only on the `LLMProvider` interface in
`src/embedded/provider.ts` — no caller changes needed to swap providers.

Refs: ANN-16, ANN-8.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@kurtstohrer kurtstohrer merged commit dbbc1df into main May 12, 2026
4 of 5 checks passed
@kurtstohrer kurtstohrer deleted the feat/ann-16-provider-plugins branch May 12, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant