Skip to content

feat(llm): add OpenCode Go provider#782

Open
eugenechin wants to merge 2 commits into
TauricResearch:mainfrom
eugenechin:pr/opencode-go-provider
Open

feat(llm): add OpenCode Go provider#782
eugenechin wants to merge 2 commits into
TauricResearch:mainfrom
eugenechin:pr/opencode-go-provider

Conversation

@eugenechin
Copy link
Copy Markdown

Summary

Wires OpenCode Go (https://opencode.ai/zen/go/v1) as a selectable LLM provider alongside the existing OpenAI-compatible providers. OpenCode Go is a paid subscription tier ($5/$10 per month) routing to ~12 open-source coding models via an OpenAI-compatible chat-completions endpoint.

Changes

  • Register opencode-go in the OpenAI-compatible factory branch and _PROVIDER_CONFIG (base URL + OPENCODE_GO_API_KEY env var).
  • Catalog all 12 routed models (deepseek-v4-{flash,pro}, kimi-k2.{5,6}, glm-{5,5.1}, qwen3.{5,6}-plus, minimax-m2.{5,7}, mimo-v2.5{,-pro}), partitioned into quick/deep modes by family tier.
  • Add to interactive CLI provider selector.
  • Enable streaming for opencode-go: the gateway closes connections on long non-streamed responses (~3 min idle timeout); streaming keeps the socket active.
  • Reuse DeepSeekChatOpenAI for opencode-go (its gateway proxies DeepSeek backends, so it inherits the same reasoning_content round-trip requirement). Add streaming-path capture via _convert_chunk_to_generation_chunk since langchain-openai's stream parser drops reasoning_content from deltas, which would otherwise cause HTTP 400 on the next turn.
  • Add unit tests covering provider wiring, catalog coverage for all 12 models, validator approval, and CLI registration.

Verified end-to-end against opencode-go/deepseek-v4-flash with multi-turn tool-calling.

Test Plan

  • pytest -m unit passes (54 tests, including 8 new in tests/test_opencode_go_provider.py)
  • CLI flow: python -m cli.main lists "OpenCode Go" in the provider menu and surfaces the 12 models under quick/deep
  • End-to-end: with OPENCODE_GO_API_KEY set, python main.py (with llm_provider=opencode-go, e.g. quick_think_llm=deepseek-v4-flash, deep_think_llm=deepseek-v4-pro) completes a full SPY analysis without connection errors

🤖 Generated with Claude Code

Wires OpenCode Go (https://opencode.ai/zen/go/v1) as a selectable LLM
provider alongside the existing OpenAI-compatible providers. OpenCode Go
is a paid subscription tier ($5/$10 per month) routing to ~12 open-source
coding models via an OpenAI-compatible chat-completions endpoint.

Changes:
- Register "opencode-go" in the OpenAI-compatible factory branch and
  _PROVIDER_CONFIG (base URL + OPENCODE_GO_API_KEY env var).
- Catalog all 12 routed models (deepseek-v4-{flash,pro}, kimi-k2.{5,6},
  glm-{5,5.1}, qwen3.{5,6}-plus, minimax-m2.{5,7}, mimo-v2.5{,-pro}),
  partitioned into quick/deep modes by family tier.
- Add to interactive CLI provider selector.
- Enable streaming for opencode-go: the gateway closes connections on
  long non-streamed responses (~3 min idle timeout); streaming keeps the
  socket active.
- Reuse DeepSeekChatOpenAI for opencode-go (its gateway proxies DeepSeek
  backends, so it inherits the same reasoning_content round-trip
  requirement). Add streaming-path capture via _convert_chunk_to_generation_chunk
  since langchain-openai's stream parser drops reasoning_content from
  deltas, which would otherwise cause HTTP 400 on the next turn.
- Add unit tests covering provider wiring, catalog coverage for all 12
  models, validator approval, and CLI registration.

Verified end-to-end against opencode-go/deepseek-v4-flash with multi-turn
tool-calling.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for the OpenCode Go LLM provider, integrating it into the CLI, model catalog, and factory. Key changes include forcing streaming for OpenCode Go to avoid connection timeouts and extending the DeepSeekChatOpenAI client to preserve reasoning_content during streaming. Review feedback points out a potential AttributeError in the streaming logic when processing non-dictionary chunks and recommends refactoring a brittle test that relies on bytecode inspection for verifying CLI registration.

Comment on lines +94 to +98
choices = chunk.get("choices") or chunk.get("chunk", {}).get("choices") or []
if choices:
reasoning = (choices[0].get("delta") or {}).get("reasoning_content")
if reasoning:
gen_chunk.message.additional_kwargs["reasoning_content"] = reasoning
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of _convert_chunk_to_generation_chunk assumes that chunk is always a dictionary by calling .get(). However, when using the standard langchain-openai and openai SDK, chunk is typically an instance of ChatCompletionChunk (a Pydantic model), which does not have a .get() method. This will cause an AttributeError during streaming.

To fix this, you should handle both dictionary and object types, similar to how it's done in _create_chat_result (line 103). Additionally, the chunk.get("chunk") check seems inconsistent with the rest of the class and might be unnecessary if the provider is truly OpenAI-compatible.

Suggested change
choices = chunk.get("choices") or chunk.get("chunk", {}).get("choices") or []
if choices:
reasoning = (choices[0].get("delta") or {}).get("reasoning_content")
if reasoning:
gen_chunk.message.additional_kwargs["reasoning_content"] = reasoning
chunk_dict = chunk if isinstance(chunk, dict) else chunk.model_dump()
choices = chunk_dict.get("choices") or []
if choices:
reasoning = choices[0].get("delta", {}).get("reasoning_content")
if reasoning:
gen_chunk.message.additional_kwargs["reasoning_content"] = reasoning

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this — chunk is guaranteed to be a dict here, not a ChatCompletionChunk. langchain-openai's _stream coerces it before invoking _convert_chunk_to_generation_chunk:

# langchain_openai/chat_models/base.py, ~line 1577
for chunk in response:
    if not isinstance(chunk, dict):
        chunk = chunk.model_dump()
    generation_chunk = self._convert_chunk_to_generation_chunk(chunk, ...)

That's also why the parent method's signature is chunk: dict and why the parent itself uses chunk.get(...). The override matches the contract. End-to-end multi-turn streaming with tool calls runs without AttributeError on opencode-go/deepseek-v4-flash.

The chunk.get("chunk", {}).get("choices") fallback mirrors the parent's handling for the beta.chat.completions.stream path — keeping it lets the override stay safe if that code path is ever hit through inheritance.

Comment thread tests/test_opencode_go_provider.py Outdated
Comment on lines +91 to +99
rows = [
row
for const in select_llm_provider.__code__.co_consts
if isinstance(const, tuple)
for row in const
if isinstance(row, tuple)
and len(row) == 3
and row[0:2] == ("OpenCode Go", "opencode-go")
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This test is extremely brittle as it relies on inspecting the Python interpreter's internal bytecode constants (__code__.co_consts) to verify the contents of a local variable inside select_llm_provider. This implementation detail is not part of the public API and can change with different Python versions or minor refactors.

A better approach would be to refactor cli/utils.py to move the PROVIDERS list to a module-level constant. This would allow you to import and verify the list directly in your tests without resorting to bytecode inspection.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. Moved PROVIDERS to a module-level LLM_PROVIDERS constant in cli/utils.py and simplified the test to import it directly.

Test no longer depends on bytecode inspection to verify CLI registration;
imports the constant directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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