Skip to content

fix(opencode): append configured placeholder tools to client tools & protect precedence - #14156

Merged
diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.51from
adevwithpurpose:fix/opencode-free-tier-client-tools
Sep 29, 2026
Merged

diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.51from
adevwithpurpose:fix/opencode-free-tier-client-tools

Conversation

@adevwithpurpose

@adevwithpurpose adevwithpurpose commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Default change: None for standard requests.
Behavior change: Clients supplying custom tools (e.g. DeepSeek Harness, Claude Code, Cursor, Cline sending run_code or custom functions) to OpenCode Zen free-tier models (oc/big-pickle, oc/muse-spark-1.2-contributor-free) no longer fail with HTTP 403 FreeTierError. Furthermore, un-scoped tool observations from foreign callers without session IDs no longer poison the tool cache and break subsequent requests.

Background & Root Cause

  1. Upstream Capability Validation: OpenCode Zen's upstream server (/zen/v1) enforces the presence of OpenCode's official client tools (glob, grep, read, edit, write, bash) for free-tier access. In fix(opencode): match the upstream free-tier request contract #14013, applyFreeTierRequestContract checked hasTools(next): if client tools existed, it fell back to baseNames = [PLACEHOLDER_TOOL_NAME] (_noop). When OpenCode Zen received tools: [run_code, _noop], it returned 403 FreeTierError: OpenCode's free tier can only be used from within OpenCode.
    • Fix: OmniRoute now filters the configured placeholder tools against existing client tool names and appends all missing required placeholders to next.tools, leaving caller tools intact and satisfying upstream.
  2. Observation Cache Poisoning: In resolvePlaceholderNames, un-scoped getObservedToolNames(provider, model) was checked before configured. When a generic caller succeeded without a session tag, its tool list (["run_code"]) was recorded as observed and subsequently overwrote the 6 configured tools, breaking later turns and bare chat requests with 403.
    • Fix: Configured placeholder names now take precedence over un-scoped generic observed tools.

Related Issues

Validation

  • Change type: provider / routing
  • Focused tests: node --import tsx --test tests/unit/opencode-free-tier-request-contract.test.ts (39/39 passing)
  • npx eslint on modified files passes with 0 errors
  • Reconciled with active release base upstream/release/v3.8.51
  • Production-code changes include updated and new automated tests in this PR

Tests Added Or Updated

  • tests/unit/opencode-free-tier-request-contract.test.ts:
    • when client-supplied tools are present, multiple configured placeholders are appended (verifies client tools like run_code are preserved while all 6 configured tools are appended).
    • configured placeholder names take precedence over un-scoped observed tools for generic clients (verifies un-scoped observations cannot override configured official placeholders).

Coverage Notes

  • Covered in open-sse/executors/opencodeFreeTierContract.ts and open-sse/executors/opencodeToolObservation.ts.
  • All 39 unit tests in opencode-free-tier-request-contract.test.ts pass cleanly.

Reviewer Notes

  • Minimal, self-contained diff (4 files).
  • Preserves full backward compatibility with bare requests and existing session-affinity tool learning.
  • Includes changelog fragment changelog.d/fixes/14156-opencode-free-tier-client-tools.md.

⚠️ base-red inherited: #14547

…protect precedence

Cherry-picked from f374fc3 (branch fix/opencode-free-tier-client-tools,
2026-09-19) onto the deployed line fix/purify-notice-cache-stable.

The upstream free tier validates the request's declared tool NAMES, and refuses
a request that does not match the OpenCode client contract with 403 "free tier
can only be used from within OpenCode". `applyFreeTierRequestContract` early
returned on `hasTools()`, so the placeholders were only injected when the caller
sent NO tools at all -- a client that sends its own tools (DeepSeek Harness
sends `run_code`) shipped without them and was refused.

- Merge the configured placeholder names (OPENCODE_FREE_TIER_PLACEHOLDER_TOOLS)
  into the client's tools instead of only filling an empty set. Client tools are
  preserved and keep their order; only names not already declared are appended,
  so the operation stays idempotent.
- Resolve configured names ahead of un-scoped observed tools in
  resolvePlaceholderNames, so a foreign caller cannot poison the observation
  store and change which names a later request borrows.
- Cover both with unit tests (multi-placeholder client appending, observation
  precedence).

Measured on the real entry point `prepareFreeTierRequest` with
OPENCODE_FREE_TIER_PLACEHOLDER_TOOLS=glob,grep,read,edit,write,bash:
  in : tools=[run_code]
  out: tools=[run_code, glob, grep, read, edit, write, bash]
Paid models and OPENCODE_FREE_TIER_REQUEST_CONTRACT=off are untouched.
@adevwithpurpose
adevwithpurpose force-pushed the fix/opencode-free-tier-client-tools branch from f374fc3 to 8ea6d25 Compare September 23, 2026 06:07
@diegosouzapw
diegosouzapw merged commit 2acb430 into diegosouzapw:release/v3.8.51 Sep 29, 2026
9 of 16 checks passed
diegosouzapw added a commit that referenced this pull request Sep 29, 2026
…tool-map side channel, audio id, CI cache key, stale contracts) (#15112)

Release-captain base-red fix (v3.8.51 release PR #11442, unit shards 5-6): three production defects (OpenCode free-tier retry could resend the exact refused body; _toolNameMap side channel left in the Gemini/Antigravity body; unsafe audio model ids reported as 'no provider found' instead of 400) plus contract propagations (#12961 cache key, #14627, #14156, #14273, #14959, #15002). All twelve reds reproduced on the tip and pass; typecheck clean.
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