| /** | |
- 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) |
source & further reading
gist.github.com — original article
An LLM skill for understanding sarcasm without asking for clarification
Show HN: Reasoning prefills on a few open models
Lucid Claude Code output style