opencode-ccgo-provider/AGENTS.md

6.9 KiB

AGENTS.md

Instructions for LLM coding agents working in this repository.

Project overview

opencode-commandcode-provider is a native AI SDK provider that connects opencode to the CommandCode /alpha/generate API. It implements LanguageModelV3 from @ai-sdk/provider@3.0.8 and is loaded by opencode from dist/index.js via a file:// npm spec.

server.py in this directory is the reference implementation of the same translation (an OpenAI-compatible HTTP proxy). When behavior is ambiguous, server.py is the source of truth for the upstream wire shape. This package is the TypeScript port; it does not run Python.

  • Language/tooling: TypeScript, ESM, tsc.
  • Runtime target: opencode's provider loader, @ai-sdk/provider@3.0.8 (V3 specification).
  • No runtime dependencies. @ai-sdk/provider is imported with import type only; the built output has no imports of its own.

Commands

Run from the repository root.

npm install        # dev deps: typescript, @types/node, @ai-sdk/provider
npm run typecheck  # tsc --noEmit
npm run build      # tsc -> dist/
npm run smoke      # live request against CommandCode (needs a key)

There is no unit-test suite. scripts/smoke.mjs is the end-to-end check.

After any change under src/, run npm run typecheck, then npm run build, then npm run smoke. A change is not done until it typechecks and the smoke test passes.

Layout

src/index.ts      Public exports: createCommandCode (named) and default.
src/model.ts      CommandCodeLanguageModel: HTTP, retries, doStream/doGenerate, event->part mapping.
src/transform.ts  LanguageModelV3CallOptions -> /alpha/generate envelope (returns a JSON string).
src/events.ts     Async iterator over the NDJSON/SSE response body.
src/usage.ts      finish event -> LanguageModelV3Usage; finish-reason unification.
src/redact.ts     Credential scrubbing for error surfaces.
src/constants.ts  Defaults, paths, headers, passthrough params, static config block.
scripts/smoke.mjs Live end-to-end check.
server.py         Reference Python proxy (do not modify unless explicitly asked).

Architecture / data flow

opencode
  └─ provider.languageModel(id).doStream(options)
       ├─ transform(options, id) ──► JSON envelope
       ├─ fetch POST {baseURL}/alpha/generate   (retry 429/5xx)
       ├─ iterateEvents(response.body)          (NDJSON lines)
       └─ map each event ──► LanguageModelV3StreamPart*
            ... always ending with a `finish` part

doGenerate calls doStream and drains the resulting stream into a LanguageModelV3GenerateResult.

Conventions

  • ESM only. Imports of local files must end in .js (NodeNext resolution), e.g. import { transform } from "./transform.js";.
  • strict TypeScript is on, including noUncheckedIndexedAccess and verbatimModuleSyntax. Type-only imports must use import type.
  • Do not add comments unless they explain a non-obvious decision. Existing comments document upstream quirks and are intentional; match that spirit, not volume.
  • Keep transform.ts output in parity with server.py transform(). If you change the envelope, explain which server.py behavior motivated it.
  • Preserve the single public factory createCommandCode. index.ts re-exports it as both a named and the default export.
  • The package must stay runtime-dependency-free. Do not add a runtime import of a package that would need node_modules at load time; opencode imports dist/index.js directly.

Invariants and gotchas

These are load-bearing. Breaking one causes silent failures in opencode.

  1. Exactly one create* export reachable from the entrypoint. opencode loads a custom npm provider by importing it, choosing the first export whose name starts with create, and calling it as fn({ name: providerID, ...options }). The result must expose languageModel(id).
  2. params.stream is always true. Sending stream: false makes upstream answer "Proxy use detected. This endpoint only serves CLI." Non-streaming clients are served by buffering the V3 stream in doGenerate.
  3. tool-call stream parts carry input as a stringified JSON string, not an object (LanguageModelV3ToolCall.input: string). Serialize with JSON.stringify. Passing an object breaks opencode's tool execution with errors like K.input.trim is not a function.
  4. Always emit a terminal finish part. If upstream ends without a finish event, synthesize one from the accumulated state. A missing finish surfaces as a truncated-response error.
  5. Drop unpaired tool ids. Any tool-call without a matching tool-result (or vice versa) must be omitted from the envelope; upstream rejects the whole request otherwise.
  6. Usage is nested in V3. Fill inputTokens.{total,noCache,cacheRead,cacheWrite} and outputTokens.{total,text,reasoning} — not flat promptTokens/completionTokens.
  7. supportedUrls is {} and stays empty. CommandsCode URLs are not fetched by the SDK.
  8. Redact before surfacing errors. Any upstream error text passed to the client must go through redact().
  9. file:// npm specs bypass install. opencode imports dist/index.js directly, so the repo must be rebuilt for opencode to see source changes.

Change workflow

  1. Read server.py for the behavior being ported and note the relevant function.
  2. Make the smallest change in the matching src/ file.
  3. npm run typecheck — fix all errors.
  4. npm run build.
  5. npm run smoke — verify text and, when relevant, tool calls.
  6. If the change affects the opencode integration, verify with opencode run (see below).
  7. Update README.md if options, features, or behavior changed. Keep AGENTS.md invariants current if you discover a new one.

Do not edit dist/ by hand; it is generated. Do not modify server.py unless explicitly asked.

Verification

Automated:

npm run typecheck; npm run build; npm run smoke

Inside opencode (restart it first if config changed):

opencode models commandcode
opencode run "Reply with exactly: pong" -m commandcode/deepseek/deepseek-v4-flash-vision-exp
opencode run "Use the glob tool to list *.mjs in this directory and report the filenames." -m commandcode/deepseek/deepseek-v4-flash-vision-exp

The third command confirms the full tool-call round trip (tool-call emitted, executed, result replayed). Run it after touching transform.ts or the tool-call mapping in model.ts.

Do / Don't

  • Do keep the wire envelope identical to server.py.
  • Do run typecheck, build, and smoke after every source change.
  • Do prefer {env:COMMANDCODE_API_KEY} in docs and examples.
  • Don't commit real API keys, dist/, node_modules/, dump/, or logs (all gitignored).
  • Don't add runtime dependencies or make dist require node_modules.
  • Don't force stream:false, send object tool-call.input, or omit the finish part.
  • Don't commit unless the user explicitly asks.