Skip to content

Add opencode provider support alongside Claude Code settings#1

Merged
felipegaudio merged 6 commits into
mainfrom
add-opencode-support
Jul 22, 2026
Merged

Add opencode provider support alongside Claude Code settings#1
felipegaudio merged 6 commits into
mainfrom
add-opencode-support

Conversation

@felipegaudio

@felipegaudio felipegaudio commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • olist-code currently only patches ~/.claude/settings.json to point Claude Code at the local proxy. It now also writes/merges a custom OpenAI-compatible provider entry (olist-ai-gateway) into ~/.config/opencode/ (opencode.json or an existing opencode.jsonc), following opencode's documented provider schema (@ai-sdk/openai-compatible, options.baseURL/apiKey, models).
  • Existing providers/keys already in the user's opencode config are preserved; only the olist-ai-gateway provider entry is added/overwritten.
  • Harness selection (from review): init now asks which harness the user runs (claude / opencode / both) — asked right after login, before the model prompts — and only that harness's settings get written. opencode-only skips the Anthropic-specific "Opus/Sonnet/Haiku" wording (meaningless for opencode) in favor of a generic model-name prompt. run accepts --harness for non-interactive use.
  • .jsonc support (from review): if the user already has ~/.config/opencode/opencode.jsonc (opencode supports both formats), we update it in place instead of creating a separate opencode.json next to it. Known trade-off: comments in that file are stripped on write (would need a JSONC-preserving library to keep them — not added since there's no dependency for it today).
  • Bundled bugfix: non-streaming responses came back with empty contentopenai_to_anthropic_response() read the reply from choices[0].delta, but non-streaming responses put it under choices[0].message (delta is streaming-only). Found while validating this against a real gateway and a real opencode install; not opencode-specific, but needed for end-to-end testing.

Note: a max_tokens/max_completion_tokens dedup fix in adapter.py was tried and then reverted — olist-ai-gateway already dedupes this exact case before forwarding upstream, so duplicating that logic in olist-code (which only ever talks to the gateway) was redundant.

Test plan

  • pytest — full suite passes (60 passed), covering: update_claude_settings/update_opencode_settings (regression + new), harness dispatch (tests/test_cli.py), .jsonc detection, and the non-streaming bugfix.
  • ruff check / mypy — no new issues (one pre-existing unrelated line-length warning).
  • Real opencode binary installed locally (Homebrew, v1.18.4) and used to verify: provider recognition via opencode models for the plain .json path, the existing-.jsonc merge path (both providers show up correctly), and each harness selection (claude-only doesn't touch opencode config, opencode-only doesn't touch ~/.claude).
  • Real upstream gateway (Huawei ModelArts, GLM 5.2, not a mock) exercised end-to-end through the local proxy via both the Anthropic-style /v1/messages path (Claude Code) and the OpenAI-style /v1/chat/completions path (opencode) — confirmed real model responses come back with correct, non-empty content after the bugfix.

olist-code only patched ~/.claude/settings.json; it now also writes
the olist-ai-gateway as a custom OpenAI-compatible provider in
~/.config/opencode/opencode.json, so opencode users get the gateway
without manual setup.
Comment thread olist_code/config.py
CONFIG_DIR = Path.home() / ".olist-code-adapter"
CONFIG_FILE = CONFIG_DIR / "config.json"
CLAUDE_SETTINGS_FILE = Path.home() / ".claude" / "settings.json"
OPENCODE_SETTINGS_FILE = Path.home() / ".config" / "opencode" / "opencode.json"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

O formato do meu settings é .jsonc, o seu não?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boa pegada — confirmei rodando com opencode de verdade (instalei localmente pra testar). Agora o código detecta um opencode.jsonc já existente e atualiza ele em vez de criar um opencode.json do lado. Ressalva: os comentários do arquivo se perdem na reescrita (pra preservar precisaria de uma lib JSONC-aware, tipo commentjson, que não temos como dependência hoje — se topar, adiciono). Validei com o binário real: opencode models reconhece seu provider + o nosso, ambos no mesmo .jsonc.

Comment thread olist_code/config.py
existing["provider"] = {}

models: dict[str, Any] = {config.models.opus: {}}
if config.models.sonnet:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Estranho, o que isso tem esse modelo tem a ver com o opencode, o fluxo melhor seria você perguntar no CLI o harness do usuário, caso seja o opencode você nao precisa adaptar a configuração do claude code para o opencode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faz sentido, corrigido. init agora pergunta qual harness (claude/opencode/both) e run aceita --harness; só escrevemos o(s) settings do harness escolhido. Testei os 3 casos com o CLI real: harness=claude não cria nada em ~/.config/opencode, harness=opencode não cria ~/.claude, e both mantém o comportamento anterior.

Two issues from review:
- olist-code always wrote both Claude Code and opencode settings,
  even for users who only use one of the two harnesses. init now
  asks (and run accepts --harness), and only the selected harness's
  settings get touched.
- If the user already has an opencode.jsonc (JSONC, comments
  allowed) instead of opencode.json, we were blind to it and created
  a separate opencode.json next to it. Now we detect and update the
  existing .jsonc in place instead of creating a duplicate file.
  Comments in that file are not preserved across the edit (would
  need a JSONC-preserving writer/dependency) — known trade-off.

Verified against the real opencode binary (not just unit tests):
confirmed provider recognition via 'opencode models' for the plain
.json path, the existing-.jsonc path, and each harness selection.
Gustavo's follow-up point: the Opus/Sonnet/Haiku framing is
Anthropic-specific and meaningless when the user only configures
opencode (which just needs a flat list of model ids from the
provider, no tier concept). init now asks which harness first, and
only uses Opus/Sonnet/Haiku wording when Claude Code is involved;
opencode-only picks a generic 'primeiro/segundo/terceiro modelo'
prompt instead.
…lArts

Two issues surfaced while testing this branch against a real
OpenAI-compatible upstream (Huawei ModelArts, GLM 5.2), both
upstream of the opencode work but blocking end-to-end validation:

- The non-streaming message/delta key bug (already fixed on main via
  a separate PR) wasn't present on this branch since it forked
  before that fix landed — cherry-picked here so this branch is
  testable on its own.
- New: anthropic_to_openai() sent both max_tokens and
  max_completion_tokens with the same value. Huawei's API (and
  presumably other strict OpenAI-compatible servers) rejects
  requests that set both, since max_tokens is the deprecated
  alias for max_completion_tokens. Now only max_completion_tokens
  is sent.
Correction: sending only max_completion_tokens is not actually the
safer default. Found a documented case (langchain-ai/langchain#29954)
of self-hosted vLLM deployments rejecting max_completion_tokens
outright ('Extra inputs are not permitted'), since older/vanilla
OpenAI-compatible servers only implement the original max_tokens
field. Verified max_tokens alone also works fine against the real
Huawei ModelArts endpoint (GLM 5.2) that motivated this fix.
max_completion_tokens is only required by OpenAI's own o1/reasoning
models, not this project's target providers.
olist-code's primary/intended target is always the olist-ai-gateway,
which already dedupes this exact case before forwarding upstream
(apps/gateway/services/proxy.py: drops max_tokens when
max_completion_tokens is also present). Keeping equivalent logic here
duplicates a responsibility that belongs to the gateway — the layer
that actually knows per-provider quirks — for no current concrete
need.
@felipegaudio
felipegaudio merged commit ac337a7 into main Jul 22, 2026
@felipegaudio
felipegaudio deleted the add-opencode-support branch July 22, 2026 15:07
felipegaudio added a commit that referenced this pull request Jul 24, 2026
* Add opencode provider support alongside Claude Code settings

olist-code only patched ~/.claude/settings.json; it now also writes
the olist-ai-gateway as a custom OpenAI-compatible provider in
~/.config/opencode/opencode.json, so opencode users get the gateway
without manual setup.

* Address review: ask which harness, respect existing opencode.jsonc

Two issues from review:
- olist-code always wrote both Claude Code and opencode settings,
  even for users who only use one of the two harnesses. init now
  asks (and run accepts --harness), and only the selected harness's
  settings get touched.
- If the user already has an opencode.jsonc (JSONC, comments
  allowed) instead of opencode.json, we were blind to it and created
  a separate opencode.json next to it. Now we detect and update the
  existing .jsonc in place instead of creating a duplicate file.
  Comments in that file are not preserved across the edit (would
  need a JSONC-preserving writer/dependency) — known trade-off.

Verified against the real opencode binary (not just unit tests):
confirmed provider recognition via 'opencode models' for the plain
.json path, the existing-.jsonc path, and each harness selection.

* Ask harness before model names, use generic wording for opencode

Gustavo's follow-up point: the Opus/Sonnet/Haiku framing is
Anthropic-specific and meaningless when the user only configures
opencode (which just needs a flat list of model ids from the
provider, no tier concept). init now asks which harness first, and
only uses Opus/Sonnet/Haiku wording when Claude Code is involved;
opencode-only picks a generic 'primeiro/segundo/terceiro modelo'
prompt instead.

* Bring in real-gateway bugfixes needed to validate against Huawei ModelArts

Two issues surfaced while testing this branch against a real
OpenAI-compatible upstream (Huawei ModelArts, GLM 5.2), both
upstream of the opencode work but blocking end-to-end validation:

- The non-streaming message/delta key bug (already fixed on main via
  a separate PR) wasn't present on this branch since it forked
  before that fix landed — cherry-picked here so this branch is
  testable on its own.
- New: anthropic_to_openai() sent both max_tokens and
  max_completion_tokens with the same value. Huawei's API (and
  presumably other strict OpenAI-compatible servers) rejects
  requests that set both, since max_tokens is the deprecated
  alias for max_completion_tokens. Now only max_completion_tokens
  is sent.

* Send only max_tokens instead of max_completion_tokens

Correction: sending only max_completion_tokens is not actually the
safer default. Found a documented case (langchain-ai/langchain#29954)
of self-hosted vLLM deployments rejecting max_completion_tokens
outright ('Extra inputs are not permitted'), since older/vanilla
OpenAI-compatible servers only implement the original max_tokens
field. Verified max_tokens alone also works fine against the real
Huawei ModelArts endpoint (GLM 5.2) that motivated this fix.
max_completion_tokens is only required by OpenAI's own o1/reasoning
models, not this project's target providers.

* Revert max_tokens/max_completion_tokens dedup change

olist-code's primary/intended target is always the olist-ai-gateway,
which already dedupes this exact case before forwarding upstream
(apps/gateway/services/proxy.py: drops max_tokens when
max_completion_tokens is also present). Keeping equivalent logic here
duplicates a responsibility that belongs to the gateway — the layer
that actually knows per-provider quirks — for no current concrete
need.
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.

2 participants