146 lines
6.9 KiB
Markdown
146 lines
6.9 KiB
Markdown
# 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.
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
npm run typecheck; npm run build; npm run smoke
|
|
```
|
|
|
|
Inside opencode (restart it first if config changed):
|
|
|
|
```powershell
|
|
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.
|