# OpenCode Zen model_id strip proxy + SessionStart hook for Grok Build (mise shim)

> Source: <https://gist.github.com/km-tr/8cd435034fb848a5c1f374f0ec7b41c6>
> Published: 2026-08-06 18:26:15+00:00

|
/** |
|
* OpenCode Zen 向けローカルプロキシ |
|
* |
|
* Grok CLI が assistant 履歴に付ける非標準フィールド `model_id` を |
|
* Chat Completions 送信前に削除してから upstream へ転送する |
|
* |
|
* 起動: bun ~/.grok/proxies/opencode-strip-model-id/proxy.ts |
|
* 設定: base_url = "http://127.0.0.1:8787/v1" |
|
*/ |
|
|
|
const LISTEN_HOST = process.env.PROXY_HOST ?? "127.0.0.1" |
|
const LISTEN_PORT = Number(process.env.PROXY_PORT ?? "8787") |
|
const UPSTREAM_ORIGIN = ( |
|
process.env.OPENCODE_UPSTREAM ?? "https://opencode.ai/zen" |
|
).replace(/\/$/, "") |
|
const LOG = process.env.PROXY_LOG === "1" |
|
|
|
type JsonObject = Record<string, unknown> |
|
|
|
function isObject(value: unknown): value is JsonObject { |
|
return value != null && typeof value === "object" && !Array.isArray(value) |
|
} |
|
|
|
/** messages / input 配列内の model_id を削除する */ |
|
function stripModelIdFromMessages(messages: unknown): { |
|
stripped: number |
|
value: unknown |
|
} { |
|
if (!Array.isArray(messages)) { |
|
return { stripped: 0, value: messages } |
|
} |
|
|
|
let stripped = 0 |
|
const next = messages.map((message) => { |
|
if (!isObject(message) || !("model_id" in message)) { |
|
return message |
|
} |
|
stripped += 1 |
|
const { model_id: _modelId, ...rest } = message |
|
return rest |
|
}) |
|
return { stripped, value: next } |
|
} |
|
|
|
/** Chat Completions / Responses など JSON body から model_id を除去 */ |
|
function sanitizeRequestBody(bodyText: string): { |
|
body: string |
|
stripped: number |
|
} { |
|
if (bodyText.trim() === "") { |
|
return { body: bodyText, stripped: 0 } |
|
} |
|
|
|
let parsed: unknown |
|
try { |
|
parsed = JSON.parse(bodyText) |
|
} catch { |
|
return { body: bodyText, stripped: 0 } |
|
} |
|
|
|
if (!isObject(parsed)) { |
|
return { body: bodyText, stripped: 0 } |
|
} |
|
|
|
let stripped = 0 |
|
|
|
if ("messages" in parsed) { |
|
const result = stripModelIdFromMessages(parsed.messages) |
|
parsed.messages = result.value |
|
stripped += result.stripped |
|
} |
|
|
|
// Responses API 互換 (将来用) |
|
if ("input" in parsed) { |
|
const result = stripModelIdFromMessages(parsed.input) |
|
parsed.input = result.value |
|
stripped += result.stripped |
|
} |
|
|
|
if (stripped === 0) { |
|
return { body: bodyText, stripped: 0 } |
|
} |
|
|
|
return { body: JSON.stringify(parsed), stripped } |
|
} |
|
|
|
function hopByHopHeaders(): Set<string> { |
|
return new Set([ |
|
"connection", |
|
"keep-alive", |
|
"proxy-authenticate", |
|
"proxy-authorization", |
|
"te", |
|
"trailers", |
|
"transfer-encoding", |
|
"upgrade", |
|
"host", |
|
"content-length", |
|
]) |
|
} |
|
|
|
function copyRequestHeaders(req: Request): Headers { |
|
const headers = new Headers() |
|
const skip = hopByHopHeaders() |
|
req.headers.forEach((value, key) => { |
|
if (skip.has(key.toLowerCase())) { |
|
return |
|
} |
|
headers.set(key, value) |
|
}) |
|
return headers |
|
} |
|
|
|
function copyResponseHeaders(upstream: Response): Headers { |
|
const headers = new Headers() |
|
const skip = hopByHopHeaders() |
|
upstream.headers.forEach((value, key) => { |
|
if (skip.has(key.toLowerCase())) { |
|
return |
|
} |
|
headers.set(key, value) |
|
}) |
|
return headers |
|
} |
|
|
|
function log(...args: unknown[]): void { |
|
if (LOG) { |
|
console.log(new Date().toISOString(), ...args) |
|
} |
|
} |
|
|
|
const server = Bun.serve({ |
|
hostname: LISTEN_HOST, |
|
port: LISTEN_PORT, |
|
idleTimeout: 255, |
|
|
|
async fetch(req: Request): Promise<Response> { |
|
const url = new URL(req.url) |
|
const targetUrl = `${UPSTREAM_ORIGIN}${url.pathname}${url.search}` |
|
const method = req.method.toUpperCase() |
|
|
|
try { |
|
const requestHeaders = copyRequestHeaders(req) |
|
let body: string | Uint8Array | undefined |
|
let stripped = 0 |
|
|
|
if (method !== "GET" && method !== "HEAD") { |
|
const raw = await req.text() |
|
const shouldSanitize = |
|
url.pathname.includes("/chat/completions") || |
|
url.pathname.includes("/responses") || |
|
url.pathname.endsWith("/messages") |
|
|
|
if (shouldSanitize && raw !== "") { |
|
const sanitized = sanitizeRequestBody(raw) |
|
body = sanitized.body |
|
stripped = sanitized.stripped |
|
if (stripped > 0) { |
|
requestHeaders.set( |
|
"content-type", |
|
requestHeaders.get("content-type") ?? "application/json" |
|
) |
|
} |
|
} else { |
|
body = raw |
|
} |
|
} |
|
|
|
log( |
|
method, |
|
url.pathname, |
|
stripped > 0 ? `stripped_model_id=${stripped}` : "passthrough" |
|
) |
|
|
|
const upstream = await fetch(targetUrl, { |
|
method, |
|
headers: requestHeaders, |
|
body, |
|
// @ts-expect-error Bun は duplex を受け付ける |
|
duplex: "half", |
|
}) |
|
|
|
// ストリームはそのままパイプ (SSE / chat streaming) |
|
return new Response(upstream.body, { |
|
status: upstream.status, |
|
statusText: upstream.statusText, |
|
headers: copyResponseHeaders(upstream), |
|
}) |
|
} catch (error) { |
|
const message = error instanceof Error ? error.message : String(error) |
|
console.error("[proxy] upstream error:", message) |
|
return new Response( |
|
JSON.stringify({ |
|
error: { |
|
message: `proxy upstream failed: ${message}`, |
|
type: "proxy_error", |
|
}, |
|
}), |
|
{ |
|
status: 502, |
|
headers: { "content-type": "application/json" }, |
|
} |
|
) |
|
} |
|
}, |
|
}) |
|
|
|
console.log( |
|
`[opencode-strip-model-id] listening on http://${server.hostname}:${server.port}` |
|
) |
|
console.log(`[opencode-strip-model-id] upstream ${UPSTREAM_ORIGIN}`) |
|
console.log( |
|
`[opencode-strip-model-id] set base_url = "http://${server.hostname}:${server.port}/v1"` |
|
) |
|
console.log(`[opencode-strip-model-id] debug log: PROXY_LOG=1`) |
