{"slug": "opencode-zen-model-id-strip-proxy-sessionstart-hook-for-grok-build-mise-shim", "title": "OpenCode Zen model_id strip proxy + SessionStart hook for Grok Build (mise shim)", "summary": "A developer has created a local proxy for OpenCode Zen that strips the non-standard `model_id` field from Grok CLI assistant history before forwarding requests to the upstream Chat Completions API. The proxy, written in Bun, sanitizes JSON bodies for `/chat/completions`, `/responses`, and `/messages` endpoints, ensuring compatibility with OpenCode Zen's API.", "body_md": "|\n/** |\n|\n* OpenCode Zen 向けローカルプロキシ |\n|\n* |\n|\n* Grok CLI が assistant 履歴に付ける非標準フィールド `model_id` を |\n|\n* Chat Completions 送信前に削除してから upstream へ転送する |\n|\n* |\n|\n* 起動: bun ~/.grok/proxies/opencode-strip-model-id/proxy.ts |\n|\n* 設定: base_url = \"http://127.0.0.1:8787/v1\" |\n|\n*/ |\n|\n|\n|\nconst LISTEN_HOST = process.env.PROXY_HOST ?? \"127.0.0.1\" |\n|\nconst LISTEN_PORT = Number(process.env.PROXY_PORT ?? \"8787\") |\n|\nconst UPSTREAM_ORIGIN = ( |\n|\nprocess.env.OPENCODE_UPSTREAM ?? \"https://opencode.ai/zen\" |\n|\n).replace(/\\/$/, \"\") |\n|\nconst LOG = process.env.PROXY_LOG === \"1\" |\n|\n|\n|\ntype JsonObject = Record<string, unknown> |\n|\n|\n|\nfunction isObject(value: unknown): value is JsonObject { |\n|\nreturn value != null && typeof value === \"object\" && !Array.isArray(value) |\n|\n} |\n|\n|\n|\n/** messages / input 配列内の model_id を削除する */ |\n|\nfunction stripModelIdFromMessages(messages: unknown): { |\n|\nstripped: number |\n|\nvalue: unknown |\n|\n} { |\n|\nif (!Array.isArray(messages)) { |\n|\nreturn { stripped: 0, value: messages } |\n|\n} |\n|\n|\n|\nlet stripped = 0 |\n|\nconst next = messages.map((message) => { |\n|\nif (!isObject(message) || !(\"model_id\" in message)) { |\n|\nreturn message |\n|\n} |\n|\nstripped += 1 |\n|\nconst { model_id: _modelId, ...rest } = message |\n|\nreturn rest |\n|\n}) |\n|\nreturn { stripped, value: next } |\n|\n} |\n|\n|\n|\n/** Chat Completions / Responses など JSON body から model_id を除去 */ |\n|\nfunction sanitizeRequestBody(bodyText: string): { |\n|\nbody: string |\n|\nstripped: number |\n|\n} { |\n|\nif (bodyText.trim() === \"\") { |\n|\nreturn { body: bodyText, stripped: 0 } |\n|\n} |\n|\n|\n|\nlet parsed: unknown |\n|\ntry { |\n|\nparsed = JSON.parse(bodyText) |\n|\n} catch { |\n|\nreturn { body: bodyText, stripped: 0 } |\n|\n} |\n|\n|\n|\nif (!isObject(parsed)) { |\n|\nreturn { body: bodyText, stripped: 0 } |\n|\n} |\n|\n|\n|\nlet stripped = 0 |\n|\n|\n|\nif (\"messages\" in parsed) { |\n|\nconst result = stripModelIdFromMessages(parsed.messages) |\n|\nparsed.messages = result.value |\n|\nstripped += result.stripped |\n|\n} |\n|\n|\n|\n// Responses API 互換 (将来用) |\n|\nif (\"input\" in parsed) { |\n|\nconst result = stripModelIdFromMessages(parsed.input) |\n|\nparsed.input = result.value |\n|\nstripped += result.stripped |\n|\n} |\n|\n|\n|\nif (stripped === 0) { |\n|\nreturn { body: bodyText, stripped: 0 } |\n|\n} |\n|\n|\n|\nreturn { body: JSON.stringify(parsed), stripped } |\n|\n} |\n|\n|\n|\nfunction hopByHopHeaders(): Set<string> { |\n|\nreturn new Set([ |\n|\n\"connection\", |\n|\n\"keep-alive\", |\n|\n\"proxy-authenticate\", |\n|\n\"proxy-authorization\", |\n|\n\"te\", |\n|\n\"trailers\", |\n|\n\"transfer-encoding\", |\n|\n\"upgrade\", |\n|\n\"host\", |\n|\n\"content-length\", |\n|\n]) |\n|\n} |\n|\n|\n|\nfunction copyRequestHeaders(req: Request): Headers { |\n|\nconst headers = new Headers() |\n|\nconst skip = hopByHopHeaders() |\n|\nreq.headers.forEach((value, key) => { |\n|\nif (skip.has(key.toLowerCase())) { |\n|\nreturn |\n|\n} |\n|\nheaders.set(key, value) |\n|\n}) |\n|\nreturn headers |\n|\n} |\n|\n|\n|\nfunction copyResponseHeaders(upstream: Response): Headers { |\n|\nconst headers = new Headers() |\n|\nconst skip = hopByHopHeaders() |\n|\nupstream.headers.forEach((value, key) => { |\n|\nif (skip.has(key.toLowerCase())) { |\n|\nreturn |\n|\n} |\n|\nheaders.set(key, value) |\n|\n}) |\n|\nreturn headers |\n|\n} |\n|\n|\n|\nfunction log(...args: unknown[]): void { |\n|\nif (LOG) { |\n|\nconsole.log(new Date().toISOString(), ...args) |\n|\n} |\n|\n} |\n|\n|\n|\nconst server = Bun.serve({ |\n|\nhostname: LISTEN_HOST, |\n|\nport: LISTEN_PORT, |\n|\nidleTimeout: 255, |\n|\n|\n|\nasync fetch(req: Request): Promise<Response> { |\n|\nconst url = new URL(req.url) |\n|\nconst targetUrl = `${UPSTREAM_ORIGIN}${url.pathname}${url.search}` |\n|\nconst method = req.method.toUpperCase() |\n|\n|\n|\ntry { |\n|\nconst requestHeaders = copyRequestHeaders(req) |\n|\nlet body: string | Uint8Array | undefined |\n|\nlet stripped = 0 |\n|\n|\n|\nif (method !== \"GET\" && method !== \"HEAD\") { |\n|\nconst raw = await req.text() |\n|\nconst shouldSanitize = |\n|\nurl.pathname.includes(\"/chat/completions\") || |\n|\nurl.pathname.includes(\"/responses\") || |\n|\nurl.pathname.endsWith(\"/messages\") |\n|\n|\n|\nif (shouldSanitize && raw !== \"\") { |\n|\nconst sanitized = sanitizeRequestBody(raw) |\n|\nbody = sanitized.body |\n|\nstripped = sanitized.stripped |\n|\nif (stripped > 0) { |\n|\nrequestHeaders.set( |\n|\n\"content-type\", |\n|\nrequestHeaders.get(\"content-type\") ?? \"application/json\" |\n|\n) |\n|\n} |\n|\n} else { |\n|\nbody = raw |\n|\n} |\n|\n} |\n|\n|\n|\nlog( |\n|\nmethod, |\n|\nurl.pathname, |\n|\nstripped > 0 ? `stripped_model_id=${stripped}` : \"passthrough\" |\n|\n) |\n|\n|\n|\nconst upstream = await fetch(targetUrl, { |\n|\nmethod, |\n|\nheaders: requestHeaders, |\n|\nbody, |\n|\n// @ts-expect-error Bun は duplex を受け付ける |\n|\nduplex: \"half\", |\n|\n}) |\n|\n|\n|\n// ストリームはそのままパイプ (SSE / chat streaming) |\n|\nreturn new Response(upstream.body, { |\n|\nstatus: upstream.status, |\n|\nstatusText: upstream.statusText, |\n|\nheaders: copyResponseHeaders(upstream), |\n|\n}) |\n|\n} catch (error) { |\n|\nconst message = error instanceof Error ? error.message : String(error) |\n|\nconsole.error(\"[proxy] upstream error:\", message) |\n|\nreturn new Response( |\n|\nJSON.stringify({ |\n|\nerror: { |\n|\nmessage: `proxy upstream failed: ${message}`, |\n|\ntype: \"proxy_error\", |\n|\n}, |\n|\n}), |\n|\n{ |\n|\nstatus: 502, |\n|\nheaders: { \"content-type\": \"application/json\" }, |\n|\n} |\n|\n) |\n|\n} |\n|\n}, |\n|\n}) |\n|\n|\n|\nconsole.log( |\n|\n`[opencode-strip-model-id] listening on http://${server.hostname}:${server.port}` |\n|\n) |\n|\nconsole.log(`[opencode-strip-model-id] upstream ${UPSTREAM_ORIGIN}`) |\n|\nconsole.log( |\n|\n`[opencode-strip-model-id] set base_url = \"http://${server.hostname}:${server.port}/v1\"` |\n|\n) |\n|\nconsole.log(`[opencode-strip-model-id] debug log: PROXY_LOG=1`) |", "url": "https://wpnews.pro/news/opencode-zen-model-id-strip-proxy-sessionstart-hook-for-grok-build-mise-shim", "canonical_source": "https://gist.github.com/km-tr/8cd435034fb848a5c1f374f0ec7b41c6", "published_at": "2026-08-06 18:26:15+00:00", "updated_at": "2026-08-10 17:40:16.249793+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["OpenCode Zen", "Grok", "Bun"], "alternates": {"html": "https://wpnews.pro/news/opencode-zen-model-id-strip-proxy-sessionstart-hook-for-grok-build-mise-shim", "markdown": "https://wpnews.pro/news/opencode-zen-model-id-strip-proxy-sessionstart-hook-for-grok-build-mise-shim.md", "text": "https://wpnews.pro/news/opencode-zen-model-id-strip-proxy-sessionstart-hook-for-grok-build-mise-shim.txt", "jsonld": "https://wpnews.pro/news/opencode-zen-model-id-strip-proxy-sessionstart-hook-for-grok-build-mise-shim.jsonld"}}