OpenCode Migration Design¶
Migration plan for moving Rehor bot instances from Claude Code plus
claude-agent-sdk to OpenCode plus its TypeScript SDK. This design also
covers replacing Vertex-authenticated model traffic with an OpenAI-compatible
proxy backed by direct OpenAI API keys.
Status: Proposed
The initial migration slice is preparation only. It changes no tenant routing or model selection.
Related Jira: REHOR-35, REHOR-60, REHOR-68, REHOR-91, REHOR-92,
REHOR-125, REHOR-126, REHOR-127, REHOR-128
Decision Summary¶
- Start one runner-owned OpenCode server per cycle and close it when the cycle ends.
- Create one workspace-scoped client and session per cycle.
- Never share OpenCode servers across bot instances or cycles.
- Keep model credentials in the proxy deployment, not bot pods or OpenCode auth files.
- Add an OpenAI-compatible proxy endpoint backed by direct OpenAI API keys.
- Keep existing Jira, Git, memory, and browser MCP services separate.
- Keep Python preflights during first migration slice; migrate selected hooks to TypeScript plugins later.
- Preserve
CLAUDE.mdand.claude/skillscompatibility during canary migration.
Current Architecture¶
bot pod
Python run.py
claude-agent-sdk query()
Claude Code subprocess
CLAUDE.md + personas + .claude/skills
MCP: memory, Jira, browser
HTTP: Vertex proxy, Git proxy, Squid
central proxy pod
Vertex auth proxy → Vertex AI / Claude models
Jira MCP proxy
Git auth proxy
Executor server
Squid
Current agent entry point is bot/agent.py. Preflight scripts are discovered
and executed by bot/preflight.py. Deployment and proxy credentials are
defined in deploy/template.yaml.
Target Architecture¶
bot pod
TypeScript runner
AgentRuntime
OpenCodeRuntime
V1ServerDriver
runner-owned child `opencode serve`
loopback-bound server
one workspace-scoped client and session per cycle
SSE events: lifecycle, tool, usage, error
AGENTS.md / compatible CLAUDE.md
.opencode/agents, plugins, skills
MCP: memory, Jira, browser
HTTP: Rehor model gateway, Git proxy, Squid
central proxy pod
Rehor model gateway
Vertex route (legacy/canary)
OpenAI route (canary/new)
provider/model allowlist and policy
Jira MCP proxy
Git auth proxy
Executor server
Squid
OpenCode is an agent runtime, not replacement for every existing proxy. The proxy remains credential and egress boundary. OpenCode remains execution boundary inside each bot pod.
OpenCode Server Lifecycle¶
OpenCode supports runner-owned and client-only modes. The following is an API shape example, not production supervisor code:
import { createOpencode } from "@opencode-ai/sdk"
const opencode = await createOpencode({ hostname: "127.0.0.1", port: 4096 })
const client = opencode.client
For an already-running server:
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl: "http://127.0.0.1:4096",
})
The runner starts one server per cycle as a child process, binds it to loopback, creates one client with the cycle worktree, and closes the server during cycle cleanup. A central or persistent shared server is out of scope. OpenCode server authentication is not a substitute for Rehor task authorization.
Native TypeScript Runtime Option¶
scriptc can compile TypeScript to native executables that do not require Node, V8, or a JavaScript engine. This is a possible follow-up optimization for the Rehor runner and standalone preflight tools.
It does not mean the complete OpenCode stack can be compiled immediately:
- The scriptc compiler itself requires Node 24 or newer during image build.
- npm dependencies normally run in scriptc's embedded QuickJS dynamic tier.
@opencode-ai/sdkmust passscriptc coverage; static compilation is not assumed.- OpenCode plugins are loaded and executed by OpenCode's Bun runtime and use Bun's
$shell API. - Compiling a plugin as a separate binary does not make it an OpenCode plugin.
--dynamicremoves the Node runtime dependency but embeds a JavaScript engine.- Native networking and HTTP are supported, but SDK behavior, streaming, TLS, subprocesses, signals, and filesystem semantics need integration tests.
Viable Shape¶
build image
Node 24 + scriptc → rehor-runner native binary
runtime image
rehor-runner native binary
OpenCode official native binary or supported OpenCode runtime
no Node installation
The safest target is a native Rehor runner that connects to an already-running OpenCode server through HTTP. This avoids compiling OpenCode internals and keeps plugin loading under its supported runtime. An alternative is compiling small standalone TypeScript preflight utilities, provided they remain free of unsupported npm/runtime APIs.
Acceptance Test Before Adoption¶
scriptc coveragereports acceptable static coverage for runner entry point.scriptc buildsucceeds for target Linux architecture.- Binary runs without Node or
node_modules. - SDK client connects to OpenCode server and completes a session.
- SSE event stream handles long responses and disconnects.
- MCP configuration and tool calls remain functional.
- Signals, timeouts, child processes, Git, and filesystem behavior match current runner.
- Native and normal-runtime runs produce equivalent cycle results and cost data.
Until this test passes, use a supported OpenCode distribution and treat scriptc as an optimization experiment. Do not make native compilation a dependency of initial OpenCode migration.
Direct OpenAI Authentication¶
OpenAI API requests use HTTP Bearer authentication:
OpenAI recommends loading keys from environment variables or a secret manager, never client-side code. Rehor should keep keys in Vault and inject them only into the proxy deployment.
Preferred Flow¶
OpenCode in bot pod
→ http://devbot-proxy:8450/v1/chat/completions
→ proxy authenticates bot request
→ proxy validates provider/model
→ proxy adds Authorization: Bearer <OpenAI key>
→ https://api.openai.com/v1/chat/completions
→ streaming response back to OpenCode
The bot must not receive OPENAI_API_KEY. The OpenCode provider config should
use a bot-to-proxy credential or network policy as its authentication boundary.
If a credential is required, use a separate short-lived proxy token, not the
OpenAI key.
Implementation Shape¶
This can reuse the existing Vertex proxy pattern almost directly:
func NewOpenAIProxy(apiKey string, policy *OpenAIPolicy) http.Handler {
upstream, _ := url.Parse("https://api.openai.com")
proxy := &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(upstream)
// Client sends /v1/chat/completions; do not prepend /v1 twice.
r.Out.URL.Path = r.In.URL.Path
r.Out.URL.RawQuery = r.In.URL.RawQuery
r.Out.Host = upstream.Host
r.Out.Header.Set("Authorization", "Bearer "+apiKey)
},
FlushInterval: -1,
ModifyResponse: stripSensitiveResponseHeaders,
}
return policy.Wrap(proxy)
}
Required differences from NewVertexProxy:
- Read
OPENAI_API_KEYfrom proxy environment or secret-backed key provider. - Ignore and overwrite any incoming
Authorizationheader. - Optionally set configured
OpenAI-OrganizationandOpenAI-Projectheaders. - Validate model from JSON body for
/v1/chat/completions; URL-only model extraction is insufficient. - Allow
GET /v1/modelsonly if proxy returns an allowlisted model catalog. - Preserve SSE streaming and final usage chunks.
- Add provider-neutral metrics instead of reusing
vertex_model_requests_total.
Incoming bot authentication remains separate. Authorization from OpenCode
must authenticate the bot to Rehor, if used; it must never be forwarded to
OpenAI. Proxy then replaces it with the OpenAI key. NetworkPolicy alone can be
used for a first internal canary, but an authenticated bot-to-proxy request is
preferable before exposing the endpoint beyond the namespace.
This is header injection, not full protocol translation. It works when OpenCode emits an OpenAI-compatible request. If a future provider requires a different request or response schema, add an explicit adapter rather than silently rewriting arbitrary JSON.
Proxy Endpoint Contract¶
Initial OpenAI route should support the Chat Completions contract because it is
widely supported by OpenCode's @ai-sdk/openai-compatible provider:
POST /v1/chat/completionsGET /v1/models- streaming responses using SSE
- tool calls and tool results
- system/developer/user/assistant/tool messages
stream_options.include_usage- response usage accounting
- upstream
x-request-idcapture
Proxy requirements:
- Allow only configured model IDs.
- Reject unknown paths, hosts, and models.
- Never log
Authorization, request contents, prompts, or API keys. - Strip
Authorization,Set-Cookie,WWW-Authenticate, and token-like response headers. - Preserve streaming; do not buffer full model responses.
- Forward or generate correlation ID using
X-Client-Request-Id. - Record provider, model, HTTP status, latency, input tokens, output tokens, and errors.
- Return upstream error class without leaking credentials or secret config.
- Support key rotation without rebuilding bot images.
OpenCode Provider Configuration¶
Example bot-side opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"rehor-openai": {
"npm": "@ai-sdk/openai-compatible",
"name": "Rehor OpenAI Proxy",
"options": {
"baseURL": "http://devbot-proxy:8450/v1",
"apiKey": "{env:REHOR_MODEL_PROXY_TOKEN}"
},
"models": {
"gpt-5.4": { "name": "GPT-5.4" },
"gpt-5.4-mini": { "name": "GPT-5.4 Mini" },
"gpt-5.4-nano": { "name": "GPT-5.4 Nano" }
}
}
},
"model": "rehor-openai/gpt-5.4",
"small_model": "rehor-openai/gpt-5.4-nano",
"enabled_providers": ["rehor-openai"],
"share": "disabled"
}
Exact model IDs remain deployment configuration. Do not hardcode model names until OpenAI account access, pricing, tool support, and regional requirements are confirmed.
Provider selection remains independent from runtime selection. During canary,
OpenCode may select either the existing Vertex route or the OpenAI Chat
Completions route. If a selected model requires OpenAI Responses API semantics,
use OpenCode's native @ai-sdk/openai provider and add a separately tested
Responses route; do not translate request schemas inside header injection.
Vertex-to-OpenAI Migration¶
This is a provider and credential migration, not only a URL change.
Remove or Retire¶
GOOGLE_SA_KEY_B64from proxy deployment after rollback window.GCP_PROJECT_IDandGCP_REGIONwhere unused by remaining services.VERTEX_ALLOWED_MODELSafter all Vertex traffic is retired.- Vertex-specific request translation and token refresh code.
- Vertex-specific dashboards and alerts after equivalent OpenAI metrics exist.
Add¶
- Vault key
openai-api-keyor project-scoped equivalent. OPENAI_API_KEYproxy-only environment variable.OPENAI_ALLOWED_MODELSor provider-neutral model allowlist.OPENAI_BASE_URLwith defaulthttps://api.openai.com/v1.REHOR_MODEL_PROXY_TOKENfor bot-to-proxy authentication, if required.- OpenAI request ID and usage metrics.
- OpenAI rate-limit and retry handling.
Do not remove Vertex configuration until canary validation and rollback expiry complete. During transition, proxy may expose separate provider routes:
The bot selects provider through OpenCode config. This allows per-instance rollout without changing central routing for existing Claude-based instances.
Runtime Migration¶
Phase 1: Compatibility Canary¶
- Add OpenCode runtime and TypeScript runner beside Python runner.
- Start one OpenCode server per cycle as a runner-owned child process.
- Reuse current MCP endpoints and Git auth proxy.
- Reuse current
CLAUDE.md, personas, and.claude/skills. - Keep Python preflight scripts unchanged.
- Route model calls through OpenAI-compatible proxy.
- Run one low-risk instance with all other instances unchanged.
Phase 2: Runner Port¶
Replace claude-agent-sdk usage in bot/agent.py with SDK operations:
- create or connect to server
- create session per cycle
- submit prompt
- subscribe to SSE events
- collect assistant text, tool calls, errors, and usage
- abort session on timeout or shutdown
- close server during cycle cleanup and process termination
Preserve current CycleContext, status updates, transcript storage, cost
posting, turn limits, and signal handling.
Phase 3: Config and Persona Port¶
- Add
opencode.jsongeneration to instance config. - Map workflow model defaults to
provider/modelIDs. - Map persona prompts to OpenCode agents or
instructionsfiles. - Generate
AGENTS.mdover time; keepCLAUDE.mdfallback during rollout. - Map allowed tools to OpenCode
permissionrules. - Disable session sharing for bot instances.
Phase 4: Plugin Port¶
Port only lifecycle behavior that benefits from OpenCode hooks:
- tool policy enforcement
- environment injection
- structured logging
- session lifecycle status
- compaction context
- custom Rehor tools
Keep deterministic, no-LLM preflights and policy decisions outside OpenCode.
Existing preflight contract
(start, skip, error) is already a useful runner boundary.
Phase 5: Model Tiering¶
Map current Jira decisions:
| Rehor role | OpenCode configuration |
|---|---|
| Coding | primary agent, full model |
| Reviewer | workflow-specific model |
| Triage | subagent or separate short session |
| Researcher | subagent, standard model |
| Test writer | subagent, standard model |
| Formatter | subagent, cheap model |
Implement after basic session parity. First prove model selection and usage
accounting; then reproduce REHOR-125 and REHOR-126 routing.
Security Model¶
- OpenAI key exists only in proxy pod memory.
- Bot pod gets no OpenAI key and no Google service-account key.
- OpenCode server binds to loopback or pod-local interface.
- NetworkPolicy permits bot → model proxy only on required port.
- Server authentication is enabled if any non-loopback access exists.
shareis disabled for autonomous bot sessions.- OpenCode permissions explicitly deny unnecessary tools and providers.
- Proxy validates model and provider instead of trusting bot config.
- Proxy strips credential-bearing response headers.
- Prompt, tool arguments, and model output stay out of proxy logs by default.
- Separate bot instances use separate OpenCode servers and worktrees.
Observability¶
Preserve existing Rehor metric dimensions and add provider dimensions:
providermodelinstance_idworkflowstatusrun_id
Required measurements:
- OpenCode server health and version
- session creation and completion
- session aborts and timeouts
- MCP connection status
- model request count and status
- model latency and stream interruption
- input/output/reasoning tokens when available
- estimated cost by provider and model
- proxy rate-limit responses
- preflight duration and outcome
Do not assume OpenAI streaming always includes final usage. Store partial-cycle records and mark usage incomplete when stream terminates before final usage event.
Validation and Rollback¶
Canary acceptance criteria:
- One ticket completes from preflight through PR.
- Jira, GitHub/GitLab, memory, browser, and Slack paths work.
- Tool calls execute with equivalent permissions.
- OpenAI proxy never exposes API key to bot.
- Streaming survives long responses.
- Usage and cost data reaches dashboard.
- No credential-bearing response headers reach bot.
- Merge rate and CI pass rate are not worse than Claude baseline.
- p95 cycle duration does not regress materially.
- Model allowlist rejects unapproved IDs.
Rollback:
- Switch instance model config back to Vertex provider.
- Restart affected bot deployment.
- Leave OpenAI proxy and key intact for investigation.
- Compare OpenCode transcript, proxy metrics, and Claude baseline.
- Remove OpenCode runtime only after root cause is known.
Open Questions¶
- Which OpenAI project owns production keys and spending limits?
- Should proxy authenticate bots with service-account identity, mTLS, or a namespace-scoped token?
- Does selected OpenAI model support required tool-call and vision behavior?
- Is OpenAI data retention policy acceptable for Rehor ticket and source-code data?
- Do regional or organizational restrictions require a different OpenAI endpoint?
- Should proxy support Responses API later, or standardize on Chat Completions first?
- Which existing cost fields map reliably to OpenAI usage fields?
- Should a future persistent-volume design support server reuse across cycles?